mtg-booster-maker
Version:
Re-create boosters with your cards!
190 lines (148 loc) • 5.62 kB
JavaScript
var fs = require('fs');
var _ = require('lodash');
var csv = require('csv');
var errTree = require('err-tree');
var Promise = require('bluebird');
var chalk = require('chalk');
var argv = require('yargs')
.usage('Usage: $0 <inventory.csv> [options]')
.demand(1)
.example('$0 ~/inventory.csv -e ORI -c 3')
.describe('edition', 'Only use the given edition(s)/extension(s), can be an edition code (ORI) or an edition name ("Magic Origins")')
.describe('count', 'Create this many boosters')
.describe('noland', 'Skip land cards')
.help('h')
.alias('h', 'help')
.array('edition')
.alias('e', 'edition')
.alias('c', 'count')
.default('c', 1)
.argv
;
var data = require('./AllSets');
var BoosterError = errTree('BoosterError');
var editionNames = [];
var editions = _.transform(argv.edition, function(acc, edition) {
acc[edition] = data[edition] || _.find(data, function(ed) {
return ed.name === edition;
});
if (!acc[edition]) {
console.error('Unknown edition: ' + edition);
process.exit(1);
}
editionNames.push(acc[edition].name);
}, {});
var colors = {
'mythic rare': chalk.red.bold,
rare: chalk.yellow,
uncommon: chalk.cyan,
common: chalk.reset,
land: chalk.green,
};
function generateBooster(collection) {
var edition = _.sample(editions, 1)[0];
var subCollection = collection[edition.name];
var foiled = _.random(1, 4) === 1;
var duplicates = [];
function sample(rarity) {
if (!subCollection[rarity] || !subCollection[rarity].length) {
if (rarity !== 'mythic rare')
throw new BoosterError('ERROR: Not enough ' + rarity + ' in edition ' + edition.name + ' !');
rarity = 'rare';
}
var set = _.difference(_.pluck(subCollection[rarity], 'cardNumber'), duplicates);
var cardNumber = _.sample(set, 1)[0];
if (!cardNumber)
throw new BoosterError('ERROR: Not enough ' + rarity + ' in edition ' + edition.name + ' (would create duplicates) !');
var card = _.find(subCollection[rarity], {cardNumber: cardNumber});
card.count--;
var identifyCard = function(elt) {
return elt.cardNumber === card.cardNumber;
};
if (!card.count) {
_.remove(subCollection.all, identifyCard);
_.remove(subCollection[rarity], identifyCard);
}
if (rarity !== 'all')
duplicates.push(card.cardNumber);
return card;
}
return _.transform(edition.booster, function(acc, rarity) {
if (_.isArray(rarity)) {
// TODO: Use the probabilities by edition
rarity = _.random(1, 8) === 1 ? 'mythic rare' : 'rare';
return acc.push(sample(rarity));
}
// Add foil probability that can replace a common card with any foiled card (even rares).
// TODO: Assume a foil picks anything with the same probability. MUST be wrong.
// TODO: Assume a foil replaces a common, and in every edition. Might be wrong.
if (foiled && rarity === 'common') {
foiled = false;
return acc.push(sample('all'));
}
// TODO: Assume land is always a basic land. Might be wrong (Fate Reforged, 2nd of Return to Ravnica?)
if ((!argv.noland && rarity === 'land') || ~['common', 'uncommon', 'rare'].indexOf(rarity))
return acc.push(sample(rarity));
}, []);
}
function displayBooster(booster, i) {
if (!booster.length)
return;
var maxLength = _.reduce(booster, function(max, card) {
return Math.max(max, card.name.length);
}, 0);
function displayLine(card, color) {
if (color)
return colors[card.rarity](_.padLeft(card.cardNumber, 3, ' ')) + ' ' + colors[card.rarity](_.padRight(card.name, maxLength, ' '));
return _.padLeft(card.cardNumber, 3, ' ') + ' ' + _.padRight(card.name, maxLength, ' ');
}
var sorted = _.sortBy(booster, 'cardNumber');
var lineLength = displayLine(sorted[0]).length * 2 + 6 + sorted[0].edition.length;
var times = (lineLength - 40 - ('' + (i + 1)).length) / 2;
console.log('\n' + chalk.blue.bold('----- unsorted -' + _.repeat('-', times) + ' BOOSTER ' + (i + 1) + ' ' + _.repeat('-', times) + '- sorted -----') + '\n');
_.each(booster, function(card, i) {
console.log(displayLine(card, true) + ' ' + chalk.gray(card.edition) + ' ' + displayLine(sorted[i], true));
});
}
function parseInventory(inventory) {
return csv.parseAsync(inventory, {
skip_empty_lines: true,
auto_parse: true,
})
.then(function(lines) {
var headers = _.map(lines.shift(), _.camelCase);
return _.transform(lines, function(acc, line) {
var card = _.transform(headers, function(acc, header, i) {
acc[header] = line[i];
}, {});
if (argv.edition && !~editionNames.indexOf(card.edition))
return;
card.rarity = _.camelCase(card.rarity);
if (card.rarity === 'mythicRare')
card.rarity = 'mythic rare';
if (card.rarity === 'basicLand')
card.rarity = 'land';
acc[card.edition] = acc[card.edition] || [];
acc[card.edition].all = acc[card.edition].all || [];
acc[card.edition].all.push(card);
acc[card.edition][card.rarity] = acc[card.edition][card.rarity] || [];
acc[card.edition][card.rarity].push(card);
}, {});
})
;
}
Promise.promisifyAll(fs);
Promise.promisifyAll(csv);
fs.readFileAsync(argv._[0], 'utf8')
.then(parseInventory)
.then(function(collection) {
return _.times(argv.count, _.partial(generateBooster, collection));
})
.each(displayBooster)
.catch(BoosterError, function(err) {
console.log(err.message);
})
.catch(function(err) {
console.error(err.stack || err);
})
;