Last active
March 14, 2026 23:05
-
-
Save pakoito/5c7f9b8c35efee0126b2b874beb365db to your computer and use it in GitHub Desktop.
Revisions
-
pakoito revised this gist
Mar 14, 2026 . 1 changed file with 6 additions and 3 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -43,6 +43,7 @@ br: cost.split("{B/R}").length - 1, bg: cost.split("{B/G}").length - 1, rg: cost.split("{R/G}").length - 1, cl: cost.split("{C}").length - 1, x: cost.split("{X}").length - 1, c: parseInt(cost.substring(1, cost.length - 1).split('}{')[0]) || 0, t: 0 // storage value @@ -58,7 +59,7 @@ xValue = 1; // 3+ Xs X = 1 } // sum cc.t = cc.w + cc.u + cc.b + cc.r + cc.g + cc.wu + cc.wb + cc.wr + cc.wg + cc.ub + cc.ur + cc.ug + cc.br + cc.bg + cc.rg + cc.cl + cc.c + (cc.x * xValue); return cc; } @@ -697,14 +698,16 @@ ug: cost.ug, br: cost.br, bg: cost.bg, rg: cost.rg, cl: cost.cl, }; } function pipsToNum(cost) { return 2 ** cost.w * 3 ** cost.u * 5 ** cost.b * 7 ** cost.r * 11 ** cost.g * 13 ** cost.wu * 17 ** cost.wb * 19 ** cost.wr * 23 ** cost.wg * 29 ** cost.ub * 31 ** cost.ur * 37 ** cost.ug * 41 ** cost.br * 43 ** cost.bg * 47 ** cost.rg * 53 ** cost.cl; } function numColors(cost) { -
pakoito revised this gist
Mar 7, 2026 . 1 changed file with 1 addition and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -123,6 +123,7 @@ // staples to automatically discount const commonDiscounts = { "Blasphemous Act": 5, "Bloodsoaked Insight": 3, "The Great Henge": 4, "Treasure Cruise": 4, "Ghalta, Primal Hunger": 6, -
pakoito revised this gist
Feb 21, 2026 . 1 changed file with 0 additions and 5 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1569,7 +1569,6 @@ } } } // Generate all swaps for (let i = 0; i < activeColors.length; i++) { const fromColor = activeColors[i]; @@ -1602,7 +1601,6 @@ neighborOptions.preserveOriginalTypes = true; neighborOptions.originalLands = opts.originalLands || baseline; } while (queue.length > 0) { if (stats.tested >= opts.maxIterations) break; @@ -1710,7 +1708,6 @@ const onProgress = options.onProgress || null; const signal = options.signal || null; const preserveOriginalTypes = options.preserveOriginalTypes !== false; // Default to true const startTime = Date.now(); const cache = new Map(); const visited = new Set(); @@ -1744,7 +1741,6 @@ const topNow = extractTopResults(cache, topN); onProgress(1, 0, 'Exploring configurations...', topNow); } for await (const progress of exploreIterative(startingLands, startResult.castRate, context, cache, visited, stats, { maxIterations, signal, preserveOriginalTypes, originalLands: startingLands })) { if (onProgress) { const topNow = extractTopResults(cache, topN); @@ -1863,7 +1859,6 @@ applyIgnoreDiscount(loadResult, ignoreDiscountEntries); // Call the library's optimizer return optimizeLands$1({ loadResult, startingLands, -
pakoito revised this gist
Feb 21, 2026 . 1 changed file with 2 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1569,6 +1569,7 @@ } } } console.log('generateAllNeighbors: lands =', lands, 'protectedColors =', [...protectedColors], 'activeColors =', activeColors); // Generate all swaps for (let i = 0; i < activeColors.length; i++) { const fromColor = activeColors[i]; @@ -1743,6 +1744,7 @@ const topNow = extractTopResults(cache, topN); onProgress(1, 0, 'Exploring configurations...', topNow); } console.log('About to call exploreIterative with preserveOriginalTypes =', preserveOriginalTypes); for await (const progress of exploreIterative(startingLands, startResult.castRate, context, cache, visited, stats, { maxIterations, signal, preserveOriginalTypes, originalLands: startingLands })) { if (onProgress) { const topNow = extractTopResults(cache, topN); -
pakoito revised this gist
Feb 21, 2026 . 1 changed file with 2 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1601,6 +1601,7 @@ neighborOptions.preserveOriginalTypes = true; neighborOptions.originalLands = opts.originalLands || baseline; } console.log('exploreIterative: neighborOptions =', neighborOptions); while (queue.length > 0) { if (stats.tested >= opts.maxIterations) break; @@ -1708,6 +1709,7 @@ const onProgress = options.onProgress || null; const signal = options.signal || null; const preserveOriginalTypes = options.preserveOriginalTypes !== false; // Default to true console.log('Manatool optimizeLands: preserveOriginalTypes =', preserveOriginalTypes, 'startingLands =', startingLands); const startTime = Date.now(); const cache = new Map(); const visited = new Set(); -
pakoito revised this gist
Feb 21, 2026 . 1 changed file with 216 additions and 168 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1473,44 +1473,52 @@ } /** * Basic Land Optimizer * * Hill-climbing optimization algorithm to find optimal basic land distributions * for a given deck and manabase. */ const COLORS = ['w', 'u', 'b', 'r', 'g', 'c']; const COLOR_NAMES = { w: 'Plains', u: 'Island', b: 'Swamp', r: 'Mountain', g: 'Forest', c: 'Wastes' }; /** * Hash land configuration for cache lookups */ function hashLands$1(lands) { return COLORS.map(c => `${c.toUpperCase()}${lands[c] || 0}`).join('-'); } /** * Build land types array from basic lands configuration using existing loadResult * This preserves all the non-basic lands and just replaces the basic counts */ function buildLandTypesFromLoadResult(basicLands, loadResult, originalBasics) { // Start with a copy of the original land types (includes non-basics AND old basics) const landTypes = [...loadResult.landTypes]; // Subtract original basics and add new basics // originalBasics is the starting configuration passed to optimizer landTypes[0] = landTypes[0] - (originalBasics.c || 0) + (basicLands.c || 0); // Wastes (colorless) landTypes[1] = landTypes[1] - (originalBasics.w || 0) + (basicLands.w || 0); // Plains landTypes[2] = landTypes[2] - (originalBasics.u || 0) + (basicLands.u || 0); // Island landTypes[4] = landTypes[4] - (originalBasics.b || 0) + (basicLands.b || 0); // Swamp landTypes[8] = landTypes[8] - (originalBasics.r || 0) + (basicLands.r || 0); // Mountain landTypes[16] = landTypes[16] - (originalBasics.g || 0) + (basicLands.g || 0); // Forest return landTypes; } /** * Test a single land configuration */ async function testConfiguration(basicLands, context, cache) { const hash = hashLands$1(basicLands); // Check cache if (cache.has(hash)) { return cache.get(hash); } // Build modified loadDictResult with new basic lands // Deep copy to prevent mutation of original loadResult by deepAnal const modifiedLoadResult = { @@ -1526,248 +1534,190 @@ commander2: context.modifiedCommanders[1], commander3: context.modifiedCommanders[2] }; // Run deep analysis using the modified loadResult const deepAnalResult = await deepAnal(modifiedLoadResult, context.options); // deepAnal already returns averaged values (divided by totalCmc) const castRate = deepAnalResult.cmcOnCurve; const avgDelay = deepAnalResult.totalCmcDelay; // Store in cache const entry = { lands: { ...basicLands }, castRate: castRate, avgDelay: avgDelay }; cache.set(hash, entry); return entry; } /** * Generate all ±1 swap neighbors * * @param lands - Current land configuration * @param options - Optional settings * @param options.originalLands - Original land configuration (for preserveOriginalTypes) * @param options.preserveOriginalTypes - If true, never reduce original land types below 1 */ function generateAllNeighbors(lands, options = {}) { const neighbors = []; // Find active colors const activeColors = COLORS.filter(c => (lands[c] || 0) > 0); // Determine which colors must stay >= 1 (original types that should be preserved) const protectedColors = new Set(); if (options.preserveOriginalTypes && options.originalLands) { for (const c of COLORS) { if ((options.originalLands[c] || 0) > 0) { protectedColors.add(c); } } } // Generate all swaps for (let i = 0; i < activeColors.length; i++) { const fromColor = activeColors[i]; // Skip if this would reduce a protected color below 1 if (protectedColors.has(fromColor) && (lands[fromColor] || 0) <= 1) { continue; } for (let j = 0; j < activeColors.length; j++) { if (i === j) continue; const toColor = activeColors[j]; const neighbor = { ...lands }; neighbor[fromColor] = (neighbor[fromColor] || 0) - 1; neighbor[toColor] = (neighbor[toColor] || 0) + 1; neighbors.push(neighbor); } } return neighbors; } /** * Iterative hill-climbing exploration */ async function* exploreIterative(baseline, baselineCR, context, cache, visited, stats, opts) { const queue = [ { lands: baseline, parentCastRate: baselineCR } ]; // Build neighbor generation options const neighborOptions = {}; if (opts.preserveOriginalTypes) { neighborOptions.preserveOriginalTypes = true; neighborOptions.originalLands = opts.originalLands || baseline; } while (queue.length > 0) { if (stats.tested >= opts.maxIterations) break; if (opts.signal?.aborted) break; const current = queue.shift(); const hash = hashLands$1(current.lands); if (visited.has(hash)) continue; visited.add(hash); const currentResult = cache.get(hash); if (!currentResult) continue; const neighbors = generateAllNeighbors(current.lands, neighborOptions); for (const neighbor of neighbors) { if (stats.tested >= opts.maxIterations) break; if (opts.signal?.aborted) break; const neighborHash = hashLands$1(neighbor); let result; if (cache.has(neighborHash)) { result = cache.get(neighborHash); } else { result = await testConfiguration(neighbor, context, cache); stats.tested++; } if (result.castRate > current.parentCastRate) { stats.improved++; queue.push({ lands: neighbor, parentCastRate: result.castRate }); } yield { tested: stats.tested, improved: stats.improved }; } } } /** * Extract top N results from cache */ function extractTopResults(cache, n) { const results = Array.from(cache.values()); results.sort((a, b) => { const rateDiff = b.castRate - a.castRate; if (Math.abs(rateDiff) > 0.001) { return rateDiff; } else { return a.avgDelay - b.avgDelay; } }); return results.slice(0, n); } /** * Describe changes between two land configurations */ function describeLandChanges$1(oldLands, newLands) { const changes = []; for (const color of COLORS) { const oldCount = oldLands[color] || 0; const newCount = newLands[color] || 0; const diff = newCount - oldCount; if (diff > 0) { changes.push(`+${diff} ${COLOR_NAMES[color]}`); } else if (diff < 0) { changes.push(`${diff} ${COLOR_NAMES[color]}`); } } return changes.length > 0 ? changes.join(', ') : 'No changes'; } /** * Generate recommendations from top results */ function generateRecommendations(startingLands, topResults) { if (topResults.length === 0) return []; const recommendations = []; const best = topResults[0]; for (let i = 0; i < topResults.length; i++) { const result = topResults[i]; const changes = describeLandChanges$1(startingLands, result.lands); const castRatePct = (result.castRate * 100).toFixed(1); const castRateDiff = ((result.castRate - best.castRate) * 100).toFixed(1); const delayDiff = (result.avgDelay - best.avgDelay).toFixed(2); let rec = ''; if (i === 0) { rec = `Best: ${changes} → ${castRatePct}% cast rate, ${result.avgDelay.toFixed(2)} avg delay`; } else { rec = `${changes} → ${castRatePct}% (${castRateDiff}%), ${result.avgDelay.toFixed(2)} delay (+${delayDiff})`; } recommendations.push(rec); } return recommendations; } /** * Main optimizer function * * Takes a loaded deck and finds optimal basic land distributions using hill-climbing. */ async function optimizeLands$1(input) { const { loadResult, startingLands, modifiedDeckList, modifiedCommanders, options = {} } = input; const topN = options.topN || 3; const maxIterations = options.maxIterations || 1000; const onProgress = options.onProgress || null; const signal = options.signal || null; const preserveOriginalTypes = options.preserveOriginalTypes !== false; // Default to true const startTime = Date.now(); const cache = new Map(); const visited = new Set(); const stats = { tested: 0, improved: 0}; // Build context for testConfiguration const context = { loadResult: loadResult, originalBasics: startingLands, modifiedDeckList: modifiedDeckList, modifiedCommanders: modifiedCommanders, options: { @@ -1778,48 +1728,31 @@ cmdr3Weight: options.calculatorOptions?.cmdr3Weight || 15 } }; // Test starting configuration if (onProgress) onProgress(0, 0, 'Testing starting configuration...'); const startResult = await testConfiguration(startingLands, context, cache); stats.tested++; if (signal?.aborted) { throw new Error('Optimization cancelled'); } // Run hill-climbing if (onProgress) { const topNow = extractTopResults(cache, topN); onProgress(1, 0, 'Exploring configurations...', topNow); } for await (const progress of exploreIterative(startingLands, startResult.castRate, context, cache, visited, stats, { maxIterations, signal, preserveOriginalTypes, originalLands: startingLands })) { if (onProgress) { const topNow = extractTopResults(cache, topN); onProgress(progress.tested, maxIterations, `Tested ${progress.tested} configs, found ${progress.improved} improvements`, topNow); } // Yield to allow UI updates and check cancellation every iteration await new Promise(resolve => setTimeout(resolve, 0)); } // Extract top results const topResults = extractTopResults(cache, topN); // Generate recommendations const recommendations = generateRecommendations(startingLands, topResults); return { results: topResults, statistics: { @@ -1831,10 +1764,125 @@ }; } /** * Optimizer Wrapper for Manatool Library * * This wrapper provides the optimizer interface expected by the userscript * while using the core calculation functions from the manatool library. * * The actual optimization logic lives in manatool/src/optimizer.ts. * This wrapper handles: * - Deck loading via loadDict * - Ignore/discount list parsing and application * - Providing a simplified API for the userscript */ /** * @typedef {import('./manatool/src/index.ts').BasicLands} BasicLands * @typedef {import('./manatool/src/index.ts').OptimizeLandsResult} OptimizeLandsResult * @typedef {import('./manatool/src/index.ts').ConfigurationResult} ConfigurationResult * @typedef {import('./manatool/src/index.ts').ColorTestOptions} ColorTestOptions */ /** * @typedef {Object} WrapperOptimizeLandsOptions * @property {number} [topN] * @property {number} [maxIterations] * @property {(current: number, total: number, status: string, topResults?: ConfigurationResult[]) => void} [onProgress] * @property {AbortSignal | null} [signal] * @property {boolean} [preserveOriginalTypes] * @property {Partial<ColorTestOptions>} [calculatorOptions] */ /** * @typedef {Object} WrapperOptimizeLandsInput * @property {string} deckList * @property {string[]} [commanders] * @property {BasicLands} startingLands * @property {string} [ignoreList] * @property {WrapperOptimizeLandsOptions} [options] */ /** * Main optimizer function - userscript entry point * * This wraps the library's optimizeLands with deck loading logic. * * @param {WrapperOptimizeLandsInput} input * @returns {Promise<OptimizeLandsResult>} */ async function optimizeLands(input) { if (!input || typeof input !== 'object') { throw new Error('Input must be an object'); } const { deckList, commanders = [], startingLands, ignoreList = '', options = {} } = input; if (!deckList || typeof deckList !== 'string') { throw new Error('deckList must be a non-empty string'); } if (!startingLands || typeof startingLands !== 'object') { throw new Error('startingLands must be an object'); } const onProgress = options.onProgress || null; const signal = options.signal || null; // Load cards if (onProgress) { onProgress(0, 0, 'Loading cards from Scryfall...'); } const commanderNames = commanders.filter(c => c); const loadResult = await loadDict( deckList, commanderNames[0] || '', commanderNames[1] || '', commanderNames[2] || '' ); if (signal?.aborted) { throw new Error('Optimization cancelled'); } // Parse and apply ignore/discount settings const ignoreDiscountEntries = parseIgnoreDiscountList(ignoreList); const { deckList: modifiedDeckList, commanders: modifiedCommanders } = applyIgnoreDiscount(loadResult, ignoreDiscountEntries); // Call the library's optimizer console.log('Optimizer wrapper: preserveOriginalTypes =', options.preserveOriginalTypes); return optimizeLands$1({ loadResult, startingLands, modifiedDeckList, modifiedCommanders, options: { topN: options.topN, maxIterations: options.maxIterations, onProgress: options.onProgress, signal: options.signal, preserveOriginalTypes: options.preserveOriginalTypes, calculatorOptions: options.calculatorOptions } }); } /** @type {typeof Manatool.hashLands} */ const hashLands = hashLands$1; /** @type {typeof Manatool.describeLandChanges} */ const describeLandChanges = describeLandChanges$1; exports.describeLandChanges = describeLandChanges; exports.hashLands = hashLands; exports.optimizeLands = optimizeLands; -
pakoito revised this gist
Jan 29, 2026 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -949,7 +949,7 @@ comboReqs[j] -= Math.sign(j & newI); } // Yield every 10k iterations to keep UI responsive if (iterCount && iterCount.ref % 5000 === 0) { await yieldABit(); } } -
pakoito revised this gist
Jan 29, 2026 . 1 changed file with 13 additions and 5 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -902,7 +902,7 @@ // This allows UI updates and event processing between heavy calculations const yieldABit = async () => new Promise(resolve => setTimeout(resolve, 0)); async function landTestUniformSample(myLandTypes, numLands, myLandCount, i, min, max, comboReqs, iterCount) { if (Math.floor(max) == Math.floor(min)) { // no ticks in range return 0; @@ -935,26 +935,30 @@ let newMin = min; let newMax = min; for (let n = 0; n <= numLands && n <= myLandTypes[newI]; n++) { if (iterCount) { iterCount.ref++; } let p = drawType(myLandCount, myLandTypes[newI], numLands, n); if (p > 0) { newMin = newMax; newMax += p * span; const newSuccess = await landTestUniformSample(myLandTypes, numLands - n, myLandCount - myLandTypes[newI], newI - 1, newMin, newMax, comboReqs.slice(), iterCount); success += newSuccess; } for (let j = 0; j < 32; j++) { comboReqs[j] -= Math.sign(j & newI); } // Yield every 10k iterations to keep UI responsive if (iterCount && iterCount.ref % 10000 === 0) { await yieldABit(); } } return success; } async function pipDist(origWUBRG_MUTATED, cost, colors, samples, landAdded, landTypes, landCount, condLands, deckList) { // Create mutable reference for iteration counting across all landTestUniformSample calls const iterCount = { ref: 0 }; if (numColors(cost) == 5 && origWUBRG_MUTATED.length == 0) { for (let n = 0; n <= 12; n++) { // filter and round lands for given color combination @@ -998,10 +1002,14 @@ roundedLands = roundLands(landFilter(condAdjust(n, landTypes, landCount, condLands, deckList), cost)); } const roundedLandCount = sum(roundedLands); const result = await landTestUniformSample(roundedLands, n, roundedLandCount, 31, 0, samples, structuredClone(comboReqs), iterCount); canCast[n] = (result / samples); } } // Log iteration count for performance monitoring if (iterCount.ref > 0) { console.log(`[pipDist] landTestUniformSample iterations: ${iterCount.ref.toLocaleString()}`); } return canCast; } -
pakoito revised this gist
Jan 29, 2026 . 1 changed file with 7 additions and 3 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -902,7 +902,7 @@ // This allows UI updates and event processing between heavy calculations const yieldABit = async () => new Promise(resolve => setTimeout(resolve, 0)); async function landTestUniformSample(myLandTypes, numLands, myLandCount, i, min, max, comboReqs, iterations = 0) { if (Math.floor(max) == Math.floor(min)) { // no ticks in range return 0; @@ -935,17 +935,21 @@ let newMin = min; let newMax = min; for (let n = 0; n <= numLands && n <= myLandTypes[newI]; n++) { iterations++; let p = drawType(myLandCount, myLandTypes[newI], numLands, n); if (p > 0) { newMin = newMax; newMax += p * span; const newSuccess = await landTestUniformSample(myLandTypes, numLands - n, myLandCount - myLandTypes[newI], newI - 1, newMin, newMax, comboReqs.slice(), iterations); success += newSuccess; } for (let j = 0; j < 32; j++) { comboReqs[j] -= Math.sign(j & newI); } // Yield every 10k iterations to keep UI responsive if (iterations % 10000 === 0) { await yieldABit(); } } return success; } -
pakoito revised this gist
Jan 29, 2026 . 1 changed file with 98 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1375,6 +1375,91 @@ return { cmcOnCurve, totalCmcDelay, allCards, landCount: landCountAfterDrops4, landTypes: landTypesAfterDrops4, mullLands: mullLandsAfterDrops4 }; } /** * Parse ignore/discount list from text input * Format: * "-CARDNAME" will ignore the card * "3 CARDNAME" will discount the card by 3 mana */ function parseIgnoreDiscountList(text) { const lines = text.split('\n'); const entries = []; for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; let cardName = ""; let ignore = false; let discount = 0; // Check for ignored cards (starts with -) if (trimmed.charAt(0) === '-') { cardName = trimmed.substring(1).trim(); ignore = true; } // Check for discounted cards (starts with number) else if (trimmed.includes(' ')) { const spaceIndex = trimmed.indexOf(' '); const discountStr = trimmed.substring(0, spaceIndex); const parsedDiscount = parseInt(discountStr); if (!isNaN(parsedDiscount)) { discount = parsedDiscount; cardName = trimmed.substring(spaceIndex + 1).trim(); } } if (cardName) { entries.push({ cardName, ignore, discount }); } } return entries; } /** * Apply ignore/discount settings to deck cards */ function applyIgnoreDiscount(loadDictResult, entries) { const warnings = []; // Clone the deck list and commanders const deckList = loadDictResult.deckList.map(card => ({ ...card, ignore: false, discount: 0 })); const commander1 = loadDictResult.commander1 ? { ...loadDictResult.commander1, ignore: false, discount: 0 } : undefined; const commander2 = loadDictResult.commander2 ? { ...loadDictResult.commander2, ignore: false, discount: 0 } : undefined; const commander3 = loadDictResult.commander3 ? { ...loadDictResult.commander3, ignore: false, discount: 0 } : undefined; // Apply each entry for (const entry of entries) { const cardNameLower = entry.cardName.toLowerCase(); // Apply to deck list for (const card of deckList) { if (card.name.toLowerCase().includes(cardNameLower)) { card.ignore = entry.ignore; if (entry.discount > 0) { if (card.colorCost.t >= entry.discount) { card.discount = entry.discount; } else { card.discount = card.colorCost.t; warnings.push(`Warning: ${entry.cardName} cannot be given a generic discount of ${entry.discount}`); } } } } // Apply to commanders for (const commander of [commander1, commander2, commander3]) { if (commander && commander.name.toLowerCase().includes(cardNameLower)) { commander.ignore = entry.ignore; if (entry.discount > 0) { if (commander.colorCost.t >= entry.discount) { commander.discount = entry.discount; } else { commander.discount = commander.colorCost.t; warnings.push(`Warning: ${entry.cardName} cannot be given a generic discount of ${entry.discount}`); } } } } } return { deckList, commanders: [commander1, commander2, commander3], warnings }; } /** * Optimizer Wrapper for Manatool Library * @@ -1422,7 +1507,12 @@ // Deep copy arrays that get mutated in deepAnal landAdded: [...context.loadResult.landAdded], condLands: context.loadResult.condLands.map(arr => [...arr]), roundWUBRG: context.loadResult.roundWUBRG.map(arr => [...arr]), // Use the modified deckList and commanders with ignores/discounts applied deckList: context.modifiedDeckList, commander1: context.modifiedCommanders[0], commander2: context.modifiedCommanders[1], commander3: context.modifiedCommanders[2] }; // Run deep analysis using the modified loadResult @@ -1657,10 +1747,17 @@ throw new Error('Optimization cancelled'); } // Parse and apply ignore/discount settings const ignoreDiscountEntries = parseIgnoreDiscountList(ignoreList); const { deckList: modifiedDeckList, commanders: modifiedCommanders} = applyIgnoreDiscount(loadResult, ignoreDiscountEntries); // Build context with loadResult for testConfiguration const context = { loadResult: loadResult, originalBasics: startingLands, // Store original basics to swap in/out modifiedDeckList: modifiedDeckList, modifiedCommanders: modifiedCommanders, options: { approxColors: options.calculatorOptions?.approxColors || 5, approxSamples: options.calculatorOptions?.approxSamples || 100000, -
pakoito revised this gist
Jan 28, 2026 . 1 changed file with 0 additions and 3 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -247,9 +247,6 @@ const normalizedCmdr1 = commanderName1 || "nocard"; const normalizedCmdr2 = commanderName2 || "nocard"; const normalizedCmdr3 = commanderName3 || "nocard"; let commander1found = 0; let commander2found = 0; let commander3found = 0; -
pakoito revised this gist
Jan 28, 2026 . 1 changed file with 81 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -247,6 +247,9 @@ const normalizedCmdr1 = commanderName1 || "nocard"; const normalizedCmdr2 = commanderName2 || "nocard"; const normalizedCmdr3 = commanderName3 || "nocard"; console.log("DEBUG loadDict: normalizedCmdr1 =", normalizedCmdr1); console.log("DEBUG loadDict: normalizedCmdr2 =", normalizedCmdr2); console.log("DEBUG loadDict: normalizedCmdr3 =", normalizedCmdr3); let commander1found = 0; let commander2found = 0; let commander3found = 0; @@ -394,7 +397,41 @@ } card.ignore = false; // checking card names against commander names // Use separate if statements (not else if) so each card can be checked against all commanders let commanderMatch = false; if (normalizedCmdr1 !== "nocard" && card.name.toLowerCase().includes(normalizedCmdr1.toLowerCase())) { if (commander1found > 0) { deckList.push(structuredClone(commander1)); } commander1 = card; console.log("commander 1: " + card.name); commander1found += 1; commanderMatch = true; } if (normalizedCmdr2 !== "nocard" && card.name.toLowerCase().includes(normalizedCmdr2.toLowerCase())) { if (commander2found > 0) { deckList.push(structuredClone(commander2)); } commander2 = card; console.log("commander 2: " + card.name); commander2found += 1; commanderMatch = true; } if (normalizedCmdr3 !== "nocard" && card.name.toLowerCase().includes(normalizedCmdr3.toLowerCase())) { if (commander3found > 0) { deckList.push(structuredClone(commander3)); } commander3 = card; console.log("commander 3: " + card.name); commander3found += 1; commanderMatch = true; } if (commanderMatch) { // Skip to next card, don't process as land or regular card continue; } // categorizing lands and incrementing category totals else if (card.card_type.includes("Land")) { // handle fetches let deckColors = Math.sign(white) + Math.sign(blue) * 2 + Math.sign(black) * 4 + Math.sign(red) * 8 + Math.sign(green) * 16; if (card.mana_source.fetch) { @@ -450,14 +487,57 @@ if (normalizedCmdr1 != "nocard" && commander1found == 0) { errors.push("Error: The name \"" + normalizedCmdr1 + "\" did not match with any of the cards in your decklist. Double check the spelling and capitalization and try again"); } // if multiple cards match with the commander, go back and check for exact matches, warn user if there aren't any if (commander1found > 1) { let exactMatch = (commander1.name.toLowerCase() == normalizedCmdr1.toLowerCase()); for (let i = 0; i < deckList.length && !exactMatch; i++) { if (deckList[i].name.toLowerCase() == normalizedCmdr1.toLowerCase()) { let swap = structuredClone(deckList[i]); deckList[i] = structuredClone(commander1); commander1 = swap; exactMatch = true; } } if (!exactMatch) { errors.push("Error: The name \"" + normalizedCmdr1 + "\" matched with multiple cards in your decklist. Try entering the card's full name"); } } // repeat for partner if (normalizedCmdr2 != "nocard" && commander2found == 0) { errors.push("Error: The name \"" + normalizedCmdr2 + "\" did not match with any of the cards in your decklist. Double check the spelling and capitalization and try again"); } if (commander2found > 1) { let exactMatch = (commander2.name.toLowerCase() == normalizedCmdr2.toLowerCase()); for (let i = 0; i < deckList.length && !exactMatch; i++) { if (deckList[i].name.toLowerCase() == normalizedCmdr2.toLowerCase()) { let swap = structuredClone(deckList[i]); deckList[i] = structuredClone(commander2); commander2 = swap; exactMatch = true; } } if (!exactMatch) { errors.push("Error: The name \"" + normalizedCmdr2 + "\" matched with multiple cards in your decklist. Try entering the card's full name"); } } // repeat for companion if (normalizedCmdr3 != "nocard" && commander3found == 0) { errors.push("Error: The name \"" + normalizedCmdr3 + "\" did not match with any of the cards in your decklist. Double check the spelling and capitalization and try again"); } if (commander3found > 1) { let exactMatch = (commander3.name.toLowerCase() == normalizedCmdr3.toLowerCase()); for (let i = 0; i < deckList.length && !exactMatch; i++) { if (deckList[i].name.toLowerCase() == normalizedCmdr3.toLowerCase()) { let swap = structuredClone(deckList[i]); deckList[i] = structuredClone(commander3); commander3 = swap; exactMatch = true; } } if (!exactMatch) { errors.push("Error: The name \"" + normalizedCmdr3 + "\" matched with multiple cards in your decklist. Try entering the card's full name"); } } // Calculate deck colors const deckColors = Math.sign(white) + Math.sign(blue) + Math.sign(black) + Math.sign(red) + Math.sign(green); return { -
pakoito revised this gist
Jan 28, 2026 . 1 changed file with 32 additions and 39 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -346,7 +346,6 @@ landTypes[i] += addLandTypes[i] * count; } landCount += count; } else if (card.mana_source.choose) { // "choose a color" type mana sources (thriving, CLB gates) @@ -361,14 +360,12 @@ } } landCount += count; } else { // normal lands let landIndex = findLandIndex(card.mana_source) & deckColors; landTypes[landIndex] += count; landCount += count; } // note if land has conditions (i.e. verge lands) if (card.mana_source.cond) { @@ -377,7 +374,6 @@ let condition = card.mana_source.cond.cond; condLands.push([index1, index2, condition]); condNames.push(name); } } } @@ -411,7 +407,6 @@ landTypes[i] += addLandTypes[i] * count; } landCount += count; deckList.push(card); } else if (card.mana_source.choose) { @@ -427,15 +422,13 @@ } } landCount += count; deckList.push(card); } else { // normal lands let landIndex = findLandIndex(card.mana_source) & deckColors; landTypes[landIndex] += count; landCount += count; deckList.push(card); } // note if land has conditions (i.e. verge lands) @@ -445,7 +438,6 @@ let condition = card.mana_source.cond.cond; condLands.push([index1, index2, condition]); condNames.push(name); } } else { @@ -829,7 +821,11 @@ return success; } // Use setTimeout with a small delay to yield control back to the browser // This allows UI updates and event processing between heavy calculations const yieldABit = async () => new Promise(resolve => setTimeout(resolve, 0)); async function landTestUniformSample(myLandTypes, numLands, myLandCount, i, min, max, comboReqs) { if (Math.floor(max) == Math.floor(min)) { // no ticks in range return 0; @@ -866,17 +862,18 @@ if (p > 0) { newMin = newMax; newMax += p * span; const newSuccess = await landTestUniformSample(myLandTypes, numLands - n, myLandCount - myLandTypes[newI], newI - 1, newMin, newMax, comboReqs.slice()); success += newSuccess; } for (let j = 0; j < 32; j++) { comboReqs[j] -= Math.sign(j & newI); } await yieldABit(); } return success; } async function pipDist(origWUBRG_MUTATED, cost, colors, samples, landAdded, landTypes, landCount, condLands, deckList) { if (numColors(cost) == 5 && origWUBRG_MUTATED.length == 0) { for (let n = 0; n <= 12; n++) { // filter and round lands for given color combination @@ -920,22 +917,22 @@ roundedLands = roundLands(landFilter(condAdjust(n, landTypes, landCount, condLands, deckList), cost)); } const roundedLandCount = sum(roundedLands); const result = await landTestUniformSample(roundedLands, n, roundedLandCount, 31, 0, samples, structuredClone(comboReqs)); canCast[n] = (result / samples); } } return canCast; } async function xDropDict(x, deckList, approxColors, approxSamples, roundWUBRG, landAdded, landTypes, landCount, condLands) { const dict = new Map(); for (let i = 0; i < deckList.length; i++) { // run the numbers for cards with a relevant mana cost if (deckList[i].colorCost.t - deckList[i].discount == x) { let pips = colorReqs(deckList[i].colorCost); let pipCode = pipsToNum(pips); if (!dict.has(pipCode)) { const castDistro = await pipDist(roundWUBRG, pips, approxColors, approxSamples, landAdded, landTypes, landCount, condLands, deckList); dict.set(pipCode, castDistro.slice()); } } @@ -944,7 +941,7 @@ let pips = colorReqs(getColorCost(deckList[i].mana_source.cycling?.cost ?? "")); let pipCode = pipsToNum(pips); if (!dict.has(pipCode)) { const castDistro = await pipDist(roundWUBRG, pips, approxColors, approxSamples, landAdded, landTypes, landCount, condLands, deckList); dict.set(pipCode, castDistro.slice()); } } @@ -1099,10 +1096,6 @@ return costOut; } async function deepAnal(loadDict, options) { const manaDict = new Map(); // FIXME - Mutable state const { landCount, deckSize, deckList, commander1, commander2, commander3, roundWUBRG, landAdded, landTypes, condLands, sources } = loadDict; @@ -1116,27 +1109,27 @@ fourDropDict: new Map() }; // calculate for one drops based on mana from lands const oneDropDict = await xDropDict(1, deckList, approxColors, approxSamples, roundWUBRG, landAdded, landTypes, landCount, condLands); dropDicts = { ...dropDicts, oneDropDict }; // scan one drops for mana sources const { landTypes: landTypesAfterDrops1, landCount: landCountAfterDrops1 } = scanForSources(1, deckList, landTypes, landCount, sources, dropDicts, manaDict, deckSize, initialMullLands); // reflect newly identified sources in post-mulligan distribution const mullLandsAfterDrops1 = calcMullLands(landCountAfterDrops1, deckSize); await yieldABit(); // calculate for two drops based on mana from lands and one drops const twoDropDict = await xDropDict(2, deckList, approxColors, approxSamples, roundWUBRG, landAdded, landTypesAfterDrops1, landCountAfterDrops1, condLands); dropDicts = { ...dropDicts, twoDropDict }; // scan two drops for mana sources const { landTypes: landTypesAfterDrops2, landCount: landCountAfterDrops2 } = scanForSources(2, deckList, landTypesAfterDrops1, landCountAfterDrops1, sources, dropDicts, manaDict, deckSize, mullLandsAfterDrops1); const mullLandsAfterDrops2 = calcMullLands(landCountAfterDrops2, deckSize); await yieldABit(); // repeat for three and four drops const threeDropDict = await xDropDict(3, deckList, approxColors, approxSamples, roundWUBRG, landAdded, landTypesAfterDrops2, landCountAfterDrops2, condLands); dropDicts = { ...dropDicts, threeDropDict }; const { landTypes: landTypesAfterDrops3, landCount: landCountAfterDrops3 } = scanForSources(3, deckList, landTypesAfterDrops2, landCountAfterDrops2, sources, dropDicts, manaDict, deckSize, mullLandsAfterDrops2); const mullLandsAfterDrops3 = calcMullLands(landCountAfterDrops3, deckSize); await yieldABit(); const fourDropDict = await xDropDict(4, deckList, approxColors, approxSamples, roundWUBRG, landAdded, landTypesAfterDrops3, landCountAfterDrops3, condLands); dropDicts = { ...dropDicts, fourDropDict }; const { landTypes: landTypesAfterDrops4, landCount: landCountAfterDrops4 } = scanForSources(4, deckList, landTypesAfterDrops3, landCountAfterDrops3, sources, dropDicts, manaDict, deckSize, mullLandsAfterDrops3); const mullLandsAfterDrops4 = calcMullLands(landCountAfterDrops4, deckSize); @@ -1146,7 +1139,7 @@ let pips = colorReqs(deckList[i].colorCost); let pipCode = pipsToNum(pips); if (!manaDict.has(pipCode)) { const castDistro = await pipDist(roundWUBRG, pips, approxColors, approxSamples, landAdded, landTypesAfterDrops4, landCountAfterDrops4, condLands, deckList); manaDict.set(pipCode, castDistro.slice()); } await yieldABit(); @@ -1156,23 +1149,23 @@ let pips = colorReqs(commander1.colorCost); let pipCode = pipsToNum(pips); if (commander1.colorCost.t - commander1.discount == 1 && !oneDropDict.has(pipCode)) { const castDistro = await pipDist(roundWUBRG, pips, approxColors, approxSamples, landAdded, landTypesAfterDrops4, landCountAfterDrops4, condLands, deckList); oneDropDict.set(pipCode, castDistro.slice()); } else if (commander1.colorCost.t - commander1.discount == 2 && !twoDropDict.has(pipCode)) { const castDistro = await pipDist(roundWUBRG, pips, approxColors, approxSamples, landAdded, landTypesAfterDrops4, landCountAfterDrops4, condLands, deckList); twoDropDict.set(pipCode, castDistro.slice()); } else if (commander1.colorCost.t - commander1.discount == 3 && !threeDropDict.has(pipCode)) { const castDistro = await pipDist(roundWUBRG, pips, approxColors, approxSamples, landAdded, landTypesAfterDrops4, landCountAfterDrops4, condLands, deckList); threeDropDict.set(pipCode, castDistro.slice()); } else if (commander1.colorCost.t - commander1.discount == 4 && !fourDropDict.has(pipCode)) { const castDistro = await pipDist(roundWUBRG, pips, approxColors, approxSamples, landAdded, landTypesAfterDrops4, landCountAfterDrops4, condLands, deckList); fourDropDict.set(pipCode, castDistro.slice()); } else if (!manaDict.has(pipCode)) { const castDistro = await pipDist(roundWUBRG, pips, approxColors, approxSamples, landAdded, landTypesAfterDrops4, landCountAfterDrops4, condLands, deckList); manaDict.set(pipCode, castDistro.slice()); } await yieldABit(); @@ -1181,23 +1174,23 @@ let pips = colorReqs(commander2.colorCost); let pipCode = pipsToNum(pips); if (commander2.colorCost.t - commander2.discount == 1 && !oneDropDict.has(pipCode)) { const castDistro = await pipDist(roundWUBRG, pips, approxColors, approxSamples, landAdded, landTypesAfterDrops4, landCountAfterDrops4, condLands, deckList); oneDropDict.set(pipCode, castDistro.slice()); } else if (commander2.colorCost.t - commander2.discount == 2 && !twoDropDict.has(pipCode)) { const castDistro = await pipDist(roundWUBRG, pips, approxColors, approxSamples, landAdded, landTypesAfterDrops4, landCountAfterDrops4, condLands, deckList); twoDropDict.set(pipCode, castDistro.slice()); } else if (commander2.colorCost.t - commander2.discount == 3 && !threeDropDict.has(pipCode)) { const castDistro = await pipDist(roundWUBRG, pips, approxColors, approxSamples, landAdded, landTypesAfterDrops4, landCountAfterDrops4, condLands, deckList); threeDropDict.set(pipCode, castDistro.slice()); } else if (commander2.colorCost.t - commander2.discount == 4 && !fourDropDict.has(pipCode)) { const castDistro = await pipDist(roundWUBRG, pips, approxColors, approxSamples, landAdded, landTypesAfterDrops4, landCountAfterDrops4, condLands, deckList); fourDropDict.set(pipCode, castDistro.slice()); } else if (!manaDict.has(pipCode)) { const castDistro = await pipDist(roundWUBRG, pips, approxColors, approxSamples, landAdded, landTypesAfterDrops4, landCountAfterDrops4, condLands, deckList); manaDict.set(pipCode, castDistro.slice()); } await yieldABit(); @@ -1206,23 +1199,23 @@ let pips = colorReqs(commander3.colorCost); let pipCode = pipsToNum(pips); if (commander3.colorCost.t - commander3.discount == 1 && !oneDropDict.has(pipCode)) { const castDistro = await pipDist(roundWUBRG, pips, approxColors, approxSamples, landAdded, landTypesAfterDrops4, landCountAfterDrops4, condLands, deckList); oneDropDict.set(pipCode, castDistro.slice()); } else if (commander3.colorCost.t - commander3.discount == 2 && !twoDropDict.has(pipCode)) { const castDistro = await pipDist(roundWUBRG, pips, approxColors, approxSamples, landAdded, landTypesAfterDrops4, landCountAfterDrops4, condLands, deckList); twoDropDict.set(pipCode, castDistro.slice()); } else if (commander3.colorCost.t - commander3.discount == 3 && !threeDropDict.has(pipCode)) { const castDistro = await pipDist(roundWUBRG, pips, approxColors, approxSamples, landAdded, landTypesAfterDrops4, landCountAfterDrops4, condLands, deckList); threeDropDict.set(pipCode, castDistro.slice()); } else if (commander3.colorCost.t - commander3.discount == 4 && !fourDropDict.has(pipCode)) { const castDistro = await pipDist(roundWUBRG, pips, approxColors, approxSamples, landAdded, landTypesAfterDrops4, landCountAfterDrops4, condLands, deckList); fourDropDict.set(pipCode, castDistro.slice()); } else if (!manaDict.has(pipCode)) { const castDistro = await pipDist(roundWUBRG, pips, approxColors, approxSamples, landAdded, landTypesAfterDrops4, landCountAfterDrops4, condLands, deckList); manaDict.set(pipCode, castDistro.slice()); } await yieldABit(); -
pakoito revised this gist
Jan 28, 2026 . 1 changed file with 2 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1320,7 +1320,7 @@ function buildLandTypesFromLoadResult(basicLands, loadResult, originalBasics) { // Start with a copy of the original land types (includes non-basics AND old basics) const landTypes = [...loadResult.landTypes]; // Subtract original basics and add new basics // originalBasics is the starting configuration passed to optimizer landTypes[0] = landTypes[0] - (originalBasics.c || 0) + (basicLands.c || 0); // Wastes (colorless) @@ -1329,7 +1329,7 @@ landTypes[4] = landTypes[4] - (originalBasics.b || 0) + (basicLands.b || 0); // Swamp landTypes[8] = landTypes[8] - (originalBasics.r || 0) + (basicLands.r || 0); // Mountain landTypes[16] = landTypes[16] - (originalBasics.g || 0) + (basicLands.g || 0); // Forest return landTypes; } -
pakoito revised this gist
Jan 28, 2026 . 1 changed file with 5 additions and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1099,6 +1099,8 @@ return costOut; } // Use setTimeout with a small delay to yield control back to the browser // This allows UI updates and event processing between heavy calculations const yieldABit = async () => new Promise(resolve => setTimeout(resolve, 0)); async function deepAnal(loadDict, options) { @@ -1354,7 +1356,7 @@ }; // Run deep analysis using the modified loadResult const deepAnalResult = await deepAnal(modifiedLoadResult, context.options); // deepAnal already returns averaged values (divided by totalCmc) const castRate = deepAnalResult.cmcOnCurve; @@ -1629,7 +1631,8 @@ topNow); } // Yield to allow UI updates and check cancellation every iteration await new Promise(resolve => setTimeout(resolve, 0)); } // Extract top results -
pakoito revised this gist
Jan 28, 2026 . 1 changed file with 18 additions and 71 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -398,32 +398,7 @@ } card.ignore = false; // checking card names against commander names if (card.card_type.includes("Land")) { // handle fetches let deckColors = Math.sign(white) + Math.sign(blue) * 2 + Math.sign(black) * 4 + Math.sign(red) * 8 + Math.sign(green) * 16; if (card.mana_source.fetch) { @@ -483,57 +458,14 @@ if (normalizedCmdr1 != "nocard" && commander1found == 0) { errors.push("Error: The name \"" + normalizedCmdr1 + "\" did not match with any of the cards in your decklist. Double check the spelling and capitalization and try again"); } // repeat for partner if (normalizedCmdr2 != "nocard" && commander2found == 0) { errors.push("Error: The name \"" + normalizedCmdr2 + "\" did not match with any of the cards in your decklist. Double check the spelling and capitalization and try again"); } // repeat for companion if (normalizedCmdr3 != "nocard" && commander3found == 0) { errors.push("Error: The name \"" + normalizedCmdr3 + "\" did not match with any of the cards in your decklist. Double check the spelling and capitalization and try again"); } // Calculate deck colors const deckColors = Math.sign(white) + Math.sign(blue) + Math.sign(black) + Math.sign(red) + Math.sign(green); return { @@ -1167,7 +1099,9 @@ return costOut; } const yieldABit = async () => new Promise(resolve => setTimeout(resolve, 0)); async function deepAnal(loadDict, options) { const manaDict = new Map(); // FIXME - Mutable state const { landCount, deckSize, deckList, commander1, commander2, commander3, roundWUBRG, landAdded, landTypes, condLands, sources } = loadDict; const { approxColors, approxSamples, cmdr1Weight, cmdr2Weight, cmdr3Weight } = options; @@ -1186,21 +1120,25 @@ const { landTypes: landTypesAfterDrops1, landCount: landCountAfterDrops1 } = scanForSources(1, deckList, landTypes, landCount, sources, dropDicts, manaDict, deckSize, initialMullLands); // reflect newly identified sources in post-mulligan distribution const mullLandsAfterDrops1 = calcMullLands(landCountAfterDrops1, deckSize); await yieldABit(); // calculate for two drops based on mana from lands and one drops const twoDropDict = xDropDict(2, deckList, approxColors, approxSamples, roundWUBRG, landAdded, landTypesAfterDrops1, landCountAfterDrops1, condLands); dropDicts = { ...dropDicts, twoDropDict }; // scan two drops for mana sources const { landTypes: landTypesAfterDrops2, landCount: landCountAfterDrops2 } = scanForSources(2, deckList, landTypesAfterDrops1, landCountAfterDrops1, sources, dropDicts, manaDict, deckSize, mullLandsAfterDrops1); const mullLandsAfterDrops2 = calcMullLands(landCountAfterDrops2, deckSize); await yieldABit(); // repeat for three and four drops const threeDropDict = xDropDict(3, deckList, approxColors, approxSamples, roundWUBRG, landAdded, landTypesAfterDrops2, landCountAfterDrops2, condLands); dropDicts = { ...dropDicts, threeDropDict }; const { landTypes: landTypesAfterDrops3, landCount: landCountAfterDrops3 } = scanForSources(3, deckList, landTypesAfterDrops2, landCountAfterDrops2, sources, dropDicts, manaDict, deckSize, mullLandsAfterDrops2); const mullLandsAfterDrops3 = calcMullLands(landCountAfterDrops3, deckSize); await yieldABit(); const fourDropDict = xDropDict(4, deckList, approxColors, approxSamples, roundWUBRG, landAdded, landTypesAfterDrops3, landCountAfterDrops3, condLands); dropDicts = { ...dropDicts, fourDropDict }; const { landTypes: landTypesAfterDrops4, landCount: landCountAfterDrops4 } = scanForSources(4, deckList, landTypesAfterDrops3, landCountAfterDrops3, sources, dropDicts, manaDict, deckSize, mullLandsAfterDrops3); const mullLandsAfterDrops4 = calcMullLands(landCountAfterDrops4, deckSize); await yieldABit(); // calculate for 4 and above based on lands, 1, 2, and 3 for (let i = 0; i < deckList.length; i++) { let pips = colorReqs(deckList[i].colorCost); @@ -1209,6 +1147,7 @@ const castDistro = pipDist(roundWUBRG, pips, approxColors, approxSamples, landAdded, landTypesAfterDrops4, landCountAfterDrops4, condLands, deckList); manaDict.set(pipCode, castDistro.slice()); } await yieldABit(); } // calculate for commanders if (commander1) { @@ -1234,6 +1173,7 @@ const castDistro = pipDist(roundWUBRG, pips, approxColors, approxSamples, landAdded, landTypesAfterDrops4, landCountAfterDrops4, condLands, deckList); manaDict.set(pipCode, castDistro.slice()); } await yieldABit(); } if (commander2) { let pips = colorReqs(commander2.colorCost); @@ -1258,6 +1198,7 @@ const castDistro = pipDist(roundWUBRG, pips, approxColors, approxSamples, landAdded, landTypesAfterDrops4, landCountAfterDrops4, condLands, deckList); manaDict.set(pipCode, castDistro.slice()); } await yieldABit(); } if (commander3) { let pips = colorReqs(commander3.colorCost); @@ -1282,6 +1223,7 @@ const castDistro = pipDist(roundWUBRG, pips, approxColors, approxSamples, landAdded, landTypesAfterDrops4, landCountAfterDrops4, condLands, deckList); manaDict.set(pipCode, castDistro.slice()); } await yieldABit(); } // now that all of the mana values are calculated for and added to the dictionary, add up all the relevant info and return it let costsCovered = new Set(); @@ -1291,7 +1233,7 @@ let allCards = new Array(); // check how well deck can cast commanders if (commander1) { let weight = cmdr1Weight; // get the distribution to afford the commander's mana cost let turnCastDist = turnsToCast(commander1.colorCost, commander1.discount, dropDicts, manaDict, landCountAfterDrops4, deckSize, mullLandsAfterDrops4); let cmc = Math.min(commander1.colorCost.t - commander1.discount, 12); @@ -1305,6 +1247,7 @@ costsCovered.add(factorDiscount(commander1.mana_cost, commander1.discount)); } allCards.push({ name: commander1.name, onCurveRate, avgDelay, isCommander: true }); await yieldABit(); } if (commander2) { let weight = cmdr2Weight ?? 30; @@ -1319,6 +1262,7 @@ costsCovered.add(factorDiscount(commander2.mana_cost, commander2.discount)); } allCards.push({ name: commander2.name, onCurveRate, avgDelay, isCommander: true }); await yieldABit(); } if (commander3) { let weight = cmdr3Weight ?? 30; @@ -1333,7 +1277,9 @@ costsCovered.add(factorDiscount(commander3.mana_cost, commander3.discount)); } allCards.push({ name: commander3.name, onCurveRate, avgDelay }); await yieldABit(); } await yieldABit(); // do the same for every other card in the deck for (let i = 0; i < deckList.length; i++) { if ((!deckList[i].card_type.includes("Land") || (deckList[i].card_type.includes("//") && !(deckList[i].card_type == "Land // Land"))) && !deckList[i].ignore) { @@ -1348,6 +1294,7 @@ costsCovered.add(factorDiscount(deckList[i].mana_cost, deckList[i].discount)); allCards.push({ name: deckList[i].name, onCurveRate, avgDelay }); } await yieldABit(); } } // compute averages -
pakoito revised this gist
Jan 28, 2026 . 1 changed file with 69 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -398,7 +398,32 @@ } card.ignore = false; // checking card names against commander names if (normalizedCmdr1 !== "nocard" && card.name.toLowerCase().includes(normalizedCmdr1.toLowerCase())) { if (commander1found > 0) { deckList.push(structuredClone(commander1)); } commander1 = card; console.log("commander 1: " + card.name); commander1found += 1; } else if (normalizedCmdr2 !== "nocard" && card.name.toLowerCase().includes(normalizedCmdr2.toLowerCase())) { if (commander2found > 0) { deckList.push(structuredClone(commander2)); } commander2 = card; console.log("commander 2: " + card.name); commander2found += 1; } else if (normalizedCmdr3 !== "nocard" && card.name.toLowerCase().includes(normalizedCmdr3.toLowerCase())) { if (commander3found > 0) { deckList.push(structuredClone(commander3)); } commander3 = card; console.log("commander 3: " + card.name); commander3found += 1; } // categorizing lands and incrementing category totals else if (card.card_type.includes("Land")) { // handle fetches let deckColors = Math.sign(white) + Math.sign(blue) * 2 + Math.sign(black) * 4 + Math.sign(red) * 8 + Math.sign(green) * 16; if (card.mana_source.fetch) { @@ -458,14 +483,57 @@ if (normalizedCmdr1 != "nocard" && commander1found == 0) { errors.push("Error: The name \"" + normalizedCmdr1 + "\" did not match with any of the cards in your decklist. Double check the spelling and capitalization and try again"); } // if multiple cards match with the commander, go back and check for exact matches, warn user if there aren't any if (commander1found > 1) { let exactMatch = (commander1.name.toLowerCase() == normalizedCmdr1.toLowerCase()); for (let i = 0; i < deckList.length && !exactMatch; i++) { if (deckList[i].name.toLowerCase() == normalizedCmdr1.toLowerCase()) { let swap = structuredClone(deckList[i]); deckList[i] = structuredClone(commander1); commander1 = swap; exactMatch = true; } } if (!exactMatch) { errors.push("Error: The name \"" + normalizedCmdr1 + "\" matched with multiple cards in your decklist. Try entering the card's full name"); } } // repeat for partner if (normalizedCmdr2 != "nocard" && commander2found == 0) { errors.push("Error: The name \"" + normalizedCmdr2 + "\" did not match with any of the cards in your decklist. Double check the spelling and capitalization and try again"); } if (commander2found > 1) { let exactMatch = (commander2.name.toLowerCase() == normalizedCmdr2.toLowerCase()); for (let i = 0; i < deckList.length && !exactMatch; i++) { if (deckList[i].name.toLowerCase() == normalizedCmdr2.toLowerCase()) { let swap = structuredClone(deckList[i]); deckList[i] = structuredClone(commander2); commander2 = swap; exactMatch = true; } } if (!exactMatch) { errors.push("Error: The name \"" + normalizedCmdr2 + "\" matched with multiple cards in your decklist. Try entering the card's full name"); } } // repeat for companion if (normalizedCmdr3 != "nocard" && commander3found == 0) { errors.push("Error: The name \"" + normalizedCmdr3 + "\" did not match with any of the cards in your decklist. Double check the spelling and capitalization and try again"); } if (commander3found > 1) { let exactMatch = (commander3.name.toLowerCase() == normalizedCmdr3.toLowerCase()); for (let i = 0; i < deckList.length && !exactMatch; i++) { if (deckList[i].name.toLowerCase() == normalizedCmdr3.toLowerCase()) { let swap = structuredClone(deckList[i]); deckList[i] = structuredClone(commander3); commander3 = swap; exactMatch = true; } } if (!exactMatch) { errors.push("Error: The name \"" + normalizedCmdr3 + "\" matched with multiple cards in your decklist. Try entering the card's full name"); } } // Calculate deck colors const deckColors = Math.sign(white) + Math.sign(blue) + Math.sign(black) + Math.sign(red) + Math.sign(green); return { -
pakoito revised this gist
Jan 28, 2026 . 1 changed file with 15 additions and 13 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1290,7 +1290,7 @@ /** * Optimizer Wrapper for Manatool Library * * This wrapper provides the optimizer interface expected by the userscript * while using the core calculation functions from the manatool library. */ @@ -1300,18 +1300,19 @@ * Build land types array from basic lands configuration using existing loadResult * This preserves all the non-basic lands and just replaces the basic counts */ function buildLandTypesFromLoadResult(basicLands, loadResult, originalBasics) { // Start with a copy of the original land types (includes non-basics AND old basics) const landTypes = [...loadResult.landTypes]; // Subtract original basics and add new basics // originalBasics is the starting configuration passed to optimizer landTypes[0] = landTypes[0] - (originalBasics.c || 0) + (basicLands.c || 0); // Wastes (colorless) landTypes[1] = landTypes[1] - (originalBasics.w || 0) + (basicLands.w || 0); // Plains landTypes[2] = landTypes[2] - (originalBasics.u || 0) + (basicLands.u || 0); // Island landTypes[4] = landTypes[4] - (originalBasics.b || 0) + (basicLands.b || 0); // Swamp landTypes[8] = landTypes[8] - (originalBasics.r || 0) + (basicLands.r || 0); // Mountain landTypes[16] = landTypes[16] - (originalBasics.g || 0) + (basicLands.g || 0); // Forest return landTypes; } @@ -1330,7 +1331,7 @@ // Deep copy to prevent mutation of original loadResult by deepAnal const modifiedLoadResult = { ...context.loadResult, landTypes: buildLandTypesFromLoadResult(basicLands, context.loadResult, context.originalBasics), // Deep copy arrays that get mutated in deepAnal landAdded: [...context.loadResult.landAdded], condLands: context.loadResult.condLands.map(arr => [...arr]), @@ -1572,6 +1573,7 @@ // Build context with loadResult for testConfiguration const context = { loadResult: loadResult, originalBasics: startingLands, // Store original basics to swap in/out options: { approxColors: options.calculatorOptions?.approxColors || 5, approxSamples: options.calculatorOptions?.approxSamples || 100000, -
pakoito revised this gist
Jan 28, 2026 . 1 changed file with 55 additions and 58 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1303,15 +1303,15 @@ function buildLandTypesFromLoadResult(basicLands, loadResult) { // Start with a copy of the original land types (includes non-basics) const landTypes = [...loadResult.landTypes]; // Replace basic land counts (indices 0, 1, 2, 4, 8, 16) landTypes[0] = basicLands.c || 0; // Wastes (colorless) landTypes[1] = basicLands.w || 0; // Plains landTypes[2] = basicLands.u || 0; // Island landTypes[4] = basicLands.b || 0; // Swamp landTypes[8] = basicLands.r || 0; // Mountain landTypes[16] = basicLands.g || 0; // Forest return landTypes; } @@ -1320,12 +1320,12 @@ */ async function testConfiguration(basicLands, context, cache) { const hash = hashLands(basicLands); // Check cache if (cache.has(hash)) { return cache.get(hash); } // Build modified loadDictResult with new basic lands // Deep copy to prevent mutation of original loadResult by deepAnal const modifiedLoadResult = { @@ -1336,24 +1336,21 @@ condLands: context.loadResult.condLands.map(arr => [...arr]), roundWUBRG: context.loadResult.roundWUBRG.map(arr => [...arr]) }; // Run deep analysis using the modified loadResult const deepAnalResult = deepAnal(modifiedLoadResult, context.options); // deepAnal already returns averaged values (divided by totalCmc) const castRate = deepAnalResult.cmcOnCurve; const avgDelay = deepAnalResult.totalCmcDelay; // Store in cache const entry = { lands: { ...basicLands }, castRate: castRate, avgDelay: avgDelay }; cache.set(hash, entry); return entry; } @@ -1364,69 +1361,69 @@ function generateAllNeighbors(lands) { const neighbors = []; const colors = ['w', 'u', 'b', 'r', 'g', 'c']; // Find active colors const activeColors = colors.filter(c => (lands[c] || 0) > 0); // Generate all swaps for (let i = 0; i < activeColors.length; i++) { const fromColor = activeColors[i]; for (let j = 0; j < activeColors.length; j++) { if (i === j) continue; const toColor = activeColors[j]; const neighbor = { ...lands }; neighbor[fromColor] = (neighbor[fromColor] || 0) - 1; neighbor[toColor] = (neighbor[toColor] || 0) + 1; neighbors.push(neighbor); } } return neighbors; } /** * Iterative hill-climbing exploration */ async function* exploreIterative(baseline, baselineCR, context, cache, visited, stats, opts) { const queue = [{ lands: baseline, parentCastRate: baselineCR }]; while (queue.length > 0) { if (stats.tested >= opts.maxIterations) break; if (opts.signal?.aborted) break; const current = queue.shift(); const hash = hashLands(current.lands); if (visited.has(hash)) continue; visited.add(hash); const currentResult = cache.get(hash); if (!currentResult) continue; const neighbors = generateAllNeighbors(current.lands); for (const neighbor of neighbors) { if (stats.tested >= opts.maxIterations) break; if (opts.signal?.aborted) break; const neighborHash = hashLands(neighbor); let result; if (cache.has(neighborHash)) { result = cache.get(neighborHash); } else { result = await testConfiguration(neighbor, context, cache); stats.tested++; } if (result.castRate > current.parentCastRate) { stats.improved++; queue.push({ lands: neighbor, parentCastRate: result.castRate }); } yield { tested: stats.tested, improved: stats.improved }; } } } @@ -1444,7 +1441,7 @@ */ function extractTopResults(cache, n) { const results = Array.from(cache.values()); results.sort((a, b) => { const rateDiff = b.castRate - a.castRate; if (Math.abs(rateDiff) > 0.001) { @@ -1453,7 +1450,7 @@ return a.avgDelay - b.avgDelay; } }); return results.slice(0, n); } @@ -1470,21 +1467,21 @@ g: 'Forest', c: 'Wastes' }; const changes = []; for (const color of colors) { const oldCount = oldLands[color] || 0; const newCount = newLands[color] || 0; const diff = newCount - oldCount; if (diff > 0) { changes.push(`+${diff} ${colorNames[color]}`); } else if (diff < 0) { changes.push(`${diff} ${colorNames[color]}`); } } return changes.length > 0 ? changes.join(', ') : 'No changes'; } @@ -1493,27 +1490,27 @@ */ function generateRecommendations(startingLands, topResults) { if (topResults.length === 0) return []; const recommendations = []; const best = topResults[0]; for (let i = 0; i < topResults.length; i++) { const result = topResults[i]; const changes = describeLandChanges(startingLands, result.lands); const castRatePct = (result.castRate * 100).toFixed(1); const castRateDiff = ((result.castRate - best.castRate) * 100).toFixed(1); const delayDiff = (result.avgDelay - best.avgDelay).toFixed(2); let rec = ''; if (i === 0) { rec = `Best: ${changes} → ${castRatePct}% cast rate, ${result.avgDelay.toFixed(2)} avg delay`; } else { rec = `${changes} → ${castRatePct}% (${castRateDiff}%), ${result.avgDelay.toFixed(2)} delay (+${delayDiff})`; } recommendations.push(rec); } return recommendations; } @@ -1524,54 +1521,54 @@ if (!input || typeof input !== 'object') { throw new Error('Input must be an object'); } const { deckList, commanders = [], startingLands, ignoreList = '', options = {} } = input; if (!deckList || typeof deckList !== 'string') { throw new Error('deckList must be a non-empty string'); } if (!startingLands || typeof startingLands !== 'object') { throw new Error('startingLands must be an object'); } const topN = options.topN || 3; const maxIterations = options.maxIterations || 1000; const onProgress = options.onProgress || null; const signal = options.signal || null; const testDict = options.testDict || null; const startTime = Date.now(); const cache = new Map(); const visited = new Set(); const stats = { tested: 0, improved: 0}; // Load cards - reuse testDict if available if (onProgress) { const msg = testDict ? 'Using loaded deck data...' : 'Loading cards from Scryfall...'; onProgress(0, 0, msg); } const commanderNames = commanders.filter(c => c); const loadResult = await loadDict( deckList, commanderNames[0] || '', commanderNames[1] || '', commanderNames[2] || '' ); if (signal?.aborted) { throw new Error('Optimization cancelled'); } // Build context with loadResult for testConfiguration const context = { loadResult: loadResult, @@ -1583,22 +1580,22 @@ cmdr3Weight: options.calculatorOptions?.cmdr3Weight || 15 } }; // Test starting configuration if (onProgress) onProgress(0, 0, 'Testing starting configuration...'); const startResult = await testConfiguration(startingLands, context, cache); stats.tested++; if (signal?.aborted) { throw new Error('Optimization cancelled'); } // Run hill-climbing if (onProgress) { const topNow = extractTopResults(cache, topN); onProgress(1, 0, 'Exploring configurations...', topNow); } for await (const progress of exploreIterative( startingLands, startResult.castRate, @@ -1610,20 +1607,20 @@ )) { if (onProgress) { const topNow = extractTopResults(cache, topN); onProgress(progress.tested, maxIterations, `Tested ${progress.tested} configs, found ${progress.improved} improvements`, topNow); } await new Promise(resolve => setTimeout(resolve, 1)); } // Extract top results const topResults = extractTopResults(cache, topN); // Generate recommendations const recommendations = generateRecommendations(startingLands, topResults); return { results: topResults, statistics: { -
pakoito revised this gist
Jan 28, 2026 . 1 changed file with 6 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1327,9 +1327,14 @@ } // Build modified loadDictResult with new basic lands // Deep copy to prevent mutation of original loadResult by deepAnal const modifiedLoadResult = { ...context.loadResult, landTypes: buildLandTypesFromLoadResult(basicLands, context.loadResult), // Deep copy arrays that get mutated in deepAnal landAdded: [...context.loadResult.landAdded], condLands: context.loadResult.condLands.map(arr => [...arr]), roundWUBRG: context.loadResult.roundWUBRG.map(arr => [...arr]) }; // Recalculate landCount -
pakoito revised this gist
Jan 28, 2026 . 1 changed file with 3 additions and 3 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1338,9 +1338,9 @@ // Run deep analysis using the modified loadResult const deepAnalResult = deepAnal(modifiedLoadResult, context.options); // deepAnal already returns averaged values (divided by totalCmc) const castRate = deepAnalResult.cmcOnCurve; const avgDelay = deepAnalResult.totalCmcDelay; // Store in cache const entry = { -
pakoito revised this gist
Jan 28, 2026 . 1 changed file with 35 additions and 120 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1297,80 +1297,22 @@ /** * Build land types array from basic lands configuration using existing loadResult * This preserves all the non-basic lands and just replaces the basic counts */ function buildLandTypesFromLoadResult(basicLands, loadResult) { // Start with a copy of the original land types (includes non-basics) const landTypes = [...loadResult.landTypes]; // Replace basic land counts (indices 0, 1, 2, 4, 8, 16) landTypes[0] = basicLands.c || 0; // Wastes (colorless) landTypes[1] = basicLands.w || 0; // Plains landTypes[2] = basicLands.u || 0; // Island landTypes[4] = basicLands.b || 0; // Swamp landTypes[8] = basicLands.r || 0; // Mountain landTypes[16] = basicLands.g || 0; // Forest return landTypes; } /** @@ -1384,28 +1326,27 @@ return cache.get(hash); } // Build modified loadDictResult with new basic lands const modifiedLoadResult = { ...context.loadResult, landTypes: buildLandTypesFromLoadResult(basicLands, context.loadResult) }; // Recalculate landCount modifiedLoadResult.landCount = modifiedLoadResult.landTypes.reduce((sum, count) => sum + count, 0); // Run deep analysis using the modified loadResult const deepAnalResult = deepAnal(modifiedLoadResult, context.options); // Calculate metrics from deepAnal result const castRate = deepAnalResult.cmcOnCurve / (deepAnalResult.cmcOnCurve + deepAnalResult.totalCmcDelay); const avgDelay = deepAnalResult.totalCmcDelay / deepAnalResult.allCards.length; // Store in cache const entry = { lands: { ...basicLands }, castRate: castRate, avgDelay: avgDelay }; cache.set(hash, entry); @@ -1608,7 +1549,7 @@ tested: 0, improved: 0}; // Load cards - reuse testDict if available if (onProgress) { const msg = testDict ? 'Using loaded deck data...' : 'Loading cards from Scryfall...'; onProgress(0, 0, msg); @@ -1626,43 +1567,16 @@ throw new Error('Optimization cancelled'); } // Build context with loadResult for testConfiguration const context = { loadResult: loadResult, options: { approxColors: options.calculatorOptions?.approxColors || 5, approxSamples: options.calculatorOptions?.approxSamples || 100000, cmdr1Weight: options.calculatorOptions?.cmdr1Weight || 30, cmdr2Weight: options.calculatorOptions?.cmdr2Weight || 30, cmdr3Weight: options.calculatorOptions?.cmdr3Weight || 15 } }; // Test starting configuration @@ -1719,6 +1633,7 @@ exports.describeLandChanges = describeLandChanges; exports.extractTopResults = extractTopResults; exports.generateAllNeighbors = generateAllNeighbors; exports.generateNeighbors = generateAllNeighbors; exports.hashLands = hashLands; exports.optimizeLands = optimizeLands; -
pakoito revised this gist
Jan 28, 2026 . 1 changed file with 1271 additions and 1945 deletions.There are no files selected for viewing
-
pakoito revised this gist
Jan 28, 2026 . 1 changed file with 176 additions and 155 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -385,6 +385,42 @@ return set1; } // ============================================================================ // LAND CONFIGURATION // ============================================================================ /** * Build landTypes array from basic land counts * * Converts simple land counts to the 32-element land type array used internally. * * Land type encoding (bitmask): * - landTypes[0] = Wastes (colorless) * - landTypes[1] = Plains (W) * - landTypes[2] = Island (U) * - landTypes[4] = Swamp (B) * - landTypes[8] = Mountain (R) * - landTypes[16] = Forest (G) * - landTypes[3] = W/U dual lands, etc. * * @param {Object} landCounts - Basic land counts {w, u, b, r, g, c} * @returns {number[]} Land types array (length 32) */ /** * Calculate land index bitmask from mana source (matches website logic) * @param {Object} manaSource - Mana source with w, u, b, r, g flags * @returns {number} Bitmask index (0-31) */ function findLandIndex(manaSource) { if (!manaSource || typeof manaSource !== 'object') return 0; return (manaSource.w ? 1 : 0) | (manaSource.u ? 2 : 0) | (manaSource.b ? 4 : 0) | (manaSource.r ? 8 : 0) | (manaSource.g ? 16 : 0); } /** * Card Loader Module * @@ -1196,7 +1232,7 @@ * Based on website's getFetchEquivalent function */ function getFetchEquivalent(source, deckColors, deckColorCounts, landTypeArrays) { const myLandTypes = new Array(32).fill(0); // Calculate landIndex from source, masked to deck colors @@ -1234,12 +1270,42 @@ myLandTypes[16] += 0.5 / colors; } } // Handle nonbasic fetches (like Marsh Flats, Polluted Delta) - website logic line 956+ else if (source.fetch === "nb") { const { plainsTypes, islandTypes, swampTypes, mountainTypes, forestTypes } = landTypeArrays; const canFetch = new Array(32).fill(false); let myIndex = landIndex; // Determine which land types can be fetched for (let i = 0; i < 32; i++) { canFetch[i] = (plainsTypes[i] && source.w) || (islandTypes[i] && source.u) || (swampTypes[i] && source.b) || (mountainTypes[i] && source.r) || (forestTypes[i] && source.g); if (canFetch[i]) { // Include this land type in my index myIndex = myIndex | i; // Remove redundant options (if can fetch WR, don't count just W or just R) canFetch[i & 30] = canFetch[i & 30] && !(i & 1); // Remove if has W but not this one canFetch[i & 29] = canFetch[i & 29] && !(i & 2); // Remove if has U but not this one canFetch[i & 27] = canFetch[i & 27] && !(i & 4); // Remove if has B but not this one canFetch[i & 23] = canFetch[i & 23] && !(i & 8); // Remove if has R but not this one canFetch[i & 15] = canFetch[i & 15] && !(i & 16); // Remove if has G but not this one } } myLandTypes[myIndex] += 0.5; let numOptions = canFetch.filter(x => x).length; for (let i = 0; i < 32; i++) { if (canFetch[i]) { myLandTypes[i] += 0.5 / numOptions; } } } return myLandTypes; @@ -1258,6 +1324,56 @@ * This matches the website's behavior exactly. */ /** * Calculate turn distribution for casting a card (shared helper - matches website logic) */ function calculateTurnsToCast(colorCost, discount, mullLands, deckSize, landCount, oneDropDict, twoDropDict, threeDropDict, fourDropDict, manaDict) { const effectiveCMC = colorCost.t - discount; const pips = colorReqs(colorCost); const pipCode = pipsToNum(pips); // Get pip requirements from appropriate dict let dict; if (effectiveCMC === 1) dict = oneDropDict; else if (effectiveCMC === 2) dict = twoDropDict; else if (effectiveCMC === 3) dict = threeDropDict; else if (effectiveCMC === 4) dict = fourDropDict; else if (manaDict) dict = manaDict; else return null; const pipReqs = dict.get(pipCode); if (!pipReqs) return null; // Clone and zero out impossible turns const landReqs = pipReqs.slice(); for (let i = 0; i < effectiveCMC; i++) { landReqs[i] = 0; } // Calculate turn distribution (CDF → PDF) const canCast = new Array(16); for (let i = 0; i < effectiveCMC; i++) { canCast[i] = 0; } for (let i = effectiveCMC; i <= 15; i++) { let pCast = 0; const landDist = landsAtTurn(i, mullLands, deckSize, landCount); for (let lands = effectiveCMC; lands <= 12; lands++) { pCast += landReqs[lands] * landDist[lands]; } canCast[i] = pCast; } const pdf = new Array(16); pdf[0] = canCast[0]; for (let i = 1; i < 15; i++) { pdf[i] = canCast[i] - canCast[i - 1]; } pdf[15] = 1 - canCast[14]; return pdf; } /** * Scan cards of a given CMC for mana sources and add them to landTypes. @@ -1270,67 +1386,13 @@ * @param {Object} options - Configuration options * @returns {number} Updated land count */ function scanForSources(deckList, cmc, landTypes, landCount, deckSize, deckColors, deckColorCounts, oneDropDict, twoDropDict, threeDropDict, fourDropDict, options, landTypeTracking) { let newLandCount = landCount; // Calculate mulligan distribution with CURRENT land count const mullLands = calcMullLands(deckSize, landCount); // Use shared calculateTurnsToCast function (no manaDict for passes 1-4) for (const card of deckList) { // Skip if wrong CMC @@ -1356,8 +1418,8 @@ typeMult = 0.5; } // Get probability of casting this card on turn X using shared function const turnDist = calculateTurnsToCast(card.colorCost, card.discount || 0, mullLands, deckSize, landCount, oneDropDict, twoDropDict, threeDropDict, fourDropDict, null); if (turnDist) { // Weight by probability of having cast it by this turn @@ -1372,7 +1434,7 @@ // Handle fetch sources (like Land Tax, fetch lands) if (card.mana_source.fetch) { const fetchLandTypes = getFetchEquivalent(card.mana_source, deckColors, deckColorCounts, landTypeTracking); for (let j = 0; j < 32; j++) { landTypes[j] += fetchLandTypes[j] * count * typeMult; } @@ -1421,8 +1483,6 @@ newLandCount += addition; } } } return newLandCount; @@ -1434,21 +1494,6 @@ * @param {Object} manaSource - mana_source from card data * @returns {number|null} Land type index or null */ /** * Calculate deck metrics using iterative mana source scanning. * @@ -1461,6 +1506,8 @@ * @returns {Promise<Object>} {castRate, avgDelay, cardMetrics} */ async function calculateDeckMetricsIterative(deckList, commanders, initialLandTypes, initialLandCount, deckSize, options = {}) { // Extract landTypeTracking if provided const landTypeTracking = options.landTypeTracking || null; // Start with just lands let landTypes = [...initialLandTypes]; let landCount = initialLandCount; @@ -1546,7 +1593,7 @@ console.log('Before: landCount =', landCount); console.log('Before: landTypes[0]=%d, [4]=%d, [8]=%d, [12]=%d', landTypes[0], landTypes[4], landTypes[8], landTypes[12]); calculateForCMC(1, oneDropDict); landCount = scanForSources(deckList, 1, landTypes, landCount, deckSize, deckColors, deckColorCounts, oneDropDict, twoDropDict, threeDropDict, fourDropDict, options, landTypeTracking); console.log('After: landCount =', landCount); console.log('After: landTypes[0]=%d, [4]=%d, [8]=%d, [12]=%d', landTypes[0], landTypes[4], landTypes[8], landTypes[12]); @@ -1555,22 +1602,22 @@ console.log('Before: landCount =', landCount); console.log('Before: landTypes[0]=%d, [4]=%d, [8]=%d, [12]=%d', landTypes[0], landTypes[4], landTypes[8], landTypes[12]); calculateForCMC(2, twoDropDict); landCount = scanForSources(deckList, 2, landTypes, landCount, deckSize, deckColors, deckColorCounts, oneDropDict, twoDropDict, threeDropDict, fourDropDict, options, landTypeTracking); console.log('After: landCount =', landCount); console.log('After: landTypes[0]=%d, [4]=%d, [8]=%d, [12]=%d', landTypes[0], landTypes[4], landTypes[8], landTypes[12]); // Pass 3: Calculate 3-drops with lands + 1-2 drop ramp console.log('\n=== Pass 3: CMC 3 (lands + 1-2 drop ramp) ==='); console.log('Before: landCount =', landCount); calculateForCMC(3, threeDropDict); landCount = scanForSources(deckList, 3, landTypes, landCount, deckSize, deckColors, deckColorCounts, oneDropDict, twoDropDict, threeDropDict, fourDropDict, options, landTypeTracking); console.log('After: landCount =', landCount); // Pass 4: Calculate 4-drops with lands + 1-3 drop ramp console.log('\n=== Pass 4: CMC 4 (lands + 1-3 drop ramp) ==='); console.log('Before: landCount =', landCount); calculateForCMC(4, fourDropDict); landCount = scanForSources(deckList, 4, landTypes, landCount, deckSize, deckColors, deckColorCounts, oneDropDict, twoDropDict, threeDropDict, fourDropDict, options, landTypeTracking); console.log('After: landCount =', landCount); console.log('\n=== Final land count: ' + landCount + ' ==='); @@ -1632,58 +1679,7 @@ return manaDict; }; // Use shared calculateTurnsToCast function (with manaDict for CMC 5+) // Process commanders for (const cmd of commanders) { @@ -1700,7 +1696,7 @@ if (castDistro) { // Use pre-calculated pip distribution from dict + current mullLands (like website does) const turnDist = calculateTurnsToCast(cmd.colorCost, cmd.discount || 0, mullLands, deckSize, landCount, oneDropDict, twoDropDict, threeDropDict, fourDropDict, manaDict); if (turnDist) { const onCurveRate = turnDist[cmc] || 0; const avgDelay = mean(turnDist) - cmd.colorCost.t + (cmd.discount || 0); @@ -1745,7 +1741,7 @@ if (castDistro) { // Use pre-calculated pip distribution from dict + current mullLands (like website does) const turnDist = calculateTurnsToCast(card.colorCost, card.discount || 0, mullLands, deckSize, landCount, oneDropDict, twoDropDict, threeDropDict, fourDropDict, manaDict); if (turnDist) { const onCurveRate = turnDist[cmc] || 0; const avgDelay = mean(turnDist) - card.colorCost.t + (card.discount || 0); @@ -1818,6 +1814,13 @@ const landTypes = new Array(32).fill(0); let landCount = 0; // Track basic land types for nonbasic fetch logic (website logic line 1066+) const plainsTypes = new Array(32).fill(false); const islandTypes = new Array(32).fill(false); const swampTypes = new Array(32).fill(false); const mountainTypes = new Array(32).fill(false); const forestTypes = new Array(32).fill(false); // Add basic lands first (these are the counts being tested/used) landTypes[0] += basicLands.c || 0; // Wastes landTypes[1] += basicLands.w || 0; // Plains @@ -1829,24 +1832,33 @@ landCount += (basicLands.w || 0) + (basicLands.u || 0) + (basicLands.b || 0) + (basicLands.r || 0) + (basicLands.g || 0) + (basicLands.c || 0); // Mark basic land types if (basicLands.w > 0) plainsTypes[1] = true; if (basicLands.u > 0) islandTypes[2] = true; if (basicLands.b > 0) swampTypes[4] = true; if (basicLands.r > 0) mountainTypes[8] = true; if (basicLands.g > 0) forestTypes[16] = true; // Calculate deck colors bitmask from color counts const deckColorsBitmask = Math.sign(deckColors.white) + Math.sign(deckColors.blue) * 2 + Math.sign(deckColors.black) * 4 + Math.sign(deckColors.red) * 8 + Math.sign(deckColors.green) * 16; // First pass: note basic land types (website logic line 1113+) for (const card of deckList) { if (!card.card_type || !card.card_type.includes('Land')) continue; const landIndex = findLandIndex(card.mana_source); if (card.card_type.includes('Plains')) plainsTypes[landIndex] = true; if (card.card_type.includes('Island')) islandTypes[landIndex] = true; if (card.card_type.includes('Swamp')) swampTypes[landIndex] = true; if (card.card_type.includes('Mountain')) mountainTypes[landIndex] = true; if (card.card_type.includes('Forest')) forestTypes[landIndex] = true; } // Second pass: process non-basic lands (website logic from line 1173+) for (const card of deckList) { if (!card.card_type || !card.card_type.includes('Land')) continue; @@ -1858,7 +1870,12 @@ // Handle fetch lands if (card.mana_source && card.mana_source.fetch) { const addLandTypes = getFetchEquivalent( card.mana_source, deckColorsBitmask, deckColors, { plainsTypes, islandTypes, swampTypes, mountainTypes, forestTypes } ); for (let i = 0; i < 32; i++) { landTypes[i] += addLandTypes[i] * count; } @@ -1888,7 +1905,11 @@ } } return { landTypes, landCount, landTypeTracking: { plainsTypes, islandTypes, swampTypes, mountainTypes, forestTypes } }; } /** @@ -2131,7 +2152,7 @@ // Build landTypes using THE ONLY WAY // deckList contains only non-basics, basicLands are the test configuration const { landTypes, landCount, landTypeTracking } = buildInitialLandTypes( context.deckList, basicLands, context.deckColorCounts @@ -2144,7 +2165,7 @@ landTypes, landCount, context.deckSize, { ...context.options, landTypeTracking } ); // Store in cache @@ -2366,7 +2387,7 @@ // Version for debugging (replaced at build time) // Only set in UMD build, not when imported as ES module if (typeof ManabaseOptimizer !== 'undefined') { ManabaseOptimizer.version = '"2026-01-28T16:13:54.748Z"'; } exports.describeLandChanges = describeLandChanges; -
pakoito revised this gist
Jan 26, 2026 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -2366,7 +2366,7 @@ // Version for debugging (replaced at build time) // Only set in UMD build, not when imported as ES module if (typeof ManabaseOptimizer !== 'undefined') { ManabaseOptimizer.version = '"2026-01-26T18:30:58.382Z"'; } exports.describeLandChanges = describeLandChanges; -
pakoito revised this gist
Jan 26, 2026 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -2366,7 +2366,7 @@ // Version for debugging (replaced at build time) // Only set in UMD build, not when imported as ES module if (typeof ManabaseOptimizer !== 'undefined') { ManabaseOptimizer.version = '"2026-01-26T18:24:56.555Z"'; } exports.describeLandChanges = describeLandChanges; -
pakoito revised this gist
Jan 26, 2026 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -2366,7 +2366,7 @@ // Version for debugging (replaced at build time) // Only set in UMD build, not when imported as ES module if (typeof ManabaseOptimizer !== 'undefined') { ManabaseOptimizer.version = '"2026-01-26T18:23:40.934Z"'; } exports.describeLandChanges = describeLandChanges; -
pakoito revised this gist
Jan 26, 2026 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -2366,7 +2366,7 @@ // Version for debugging (replaced at build time) // Only set in UMD build, not when imported as ES module if (typeof ManabaseOptimizer !== 'undefined') { ManabaseOptimizer.version = '"2026-01-26T18:20:54.995Z"'; } exports.describeLandChanges = describeLandChanges; -
pakoito revised this gist
Jan 26, 2026 . 1 changed file with 3 additions and 3 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1911,10 +1911,10 @@ function applyIgnoreList(deckList, commanders, ignoreListText) { const lines = ignoreListText.split('\n'); // Reset ignore flags only (keep existing discounts from card-loader) for (const card of [...deckList, ...commanders]) { card.ignore = false; // Don't reset discount - card-loader already set commonDiscounts } // Process each line in ignore list @@ -2366,7 +2366,7 @@ // Version for debugging (replaced at build time) // Only set in UMD build, not when imported as ES module if (typeof ManabaseOptimizer !== 'undefined') { ManabaseOptimizer.version = '"2026-01-26T18:19:04.913Z"'; } exports.describeLandChanges = describeLandChanges; -
pakoito revised this gist
Jan 26, 2026 . 1 changed file with 62 additions and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1901,6 +1901,61 @@ */ /** * Apply ignore/discount list to deck cards (matching website logic) * * @param {Array} deckList - Deck list array * @param {Array} commanders - Commander array * @param {string} ignoreListText - Ignore/discount list text */ function applyIgnoreList(deckList, commanders, ignoreListText) { const lines = ignoreListText.split('\n'); // Reset all cards for (const card of [...deckList, ...commanders]) { card.discount = 0; card.ignore = false; } // Process each line in ignore list for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; let cardName = ''; let ignore = false; let discount = 0; // Check for ignored cards (starts with -) if (trimmed.charAt(0) === '-') { cardName = trimmed.substring(1).trim(); ignore = true; } // Check for discounted cards (number followed by space) else if (trimmed.includes(' ')) { const spaceIndex = trimmed.indexOf(' '); const discountStr = trimmed.substring(0, spaceIndex); if (!isNaN(discountStr)) { discount = parseInt(discountStr); cardName = trimmed.substring(spaceIndex + 1).trim(); } } if (!cardName) continue; // Apply to all matching cards (partial match, case-insensitive) const nameLower = cardName.toLowerCase(); for (const card of [...deckList, ...commanders]) { if (card.name.toLowerCase().includes(nameLower)) { card.ignore = ignore; if (discount > 0 && card.colorCost && card.colorCost.t >= discount) { card.discount = discount; } } } } } /** * Optimize basic land distribution using hill-climbing algorithm. * @@ -1929,6 +1984,7 @@ deckList, commanders = [], startingLands, ignoreList = '', options = {} } = input; @@ -1966,6 +2022,11 @@ throw new Error('Optimization cancelled'); } // Apply ignore/discount list to loaded cards if (ignoreList) { applyIgnoreList(loadResult.deckList, loadResult.commanders, ignoreList); } // Calculate deck size const entries = parseDeckList(deckList); const deckSize = entries.reduce((sum, e) => sum + e.quantity, 0); @@ -2305,7 +2366,7 @@ // Version for debugging (replaced at build time) // Only set in UMD build, not when imported as ES module if (typeof ManabaseOptimizer !== 'undefined') { ManabaseOptimizer.version = '"2026-01-26T18:12:28.944Z"'; } exports.describeLandChanges = describeLandChanges;
NewerOlder