Skip to content

Instantly share code, notes, and snippets.

@oggy
Created January 29, 2017 04:48
Show Gist options
  • Save oggy/bea9e3e64636c617e5b40697c1ee9912 to your computer and use it in GitHub Desktop.
Save oggy/bea9e3e64636c617e5b40697c1ee9912 to your computer and use it in GitHub Desktop.
Generate a Magic: The Gathering deck from a Cockatrice card database.
#!/usr/bin/env ruby
require 'optparse'
require 'ostruct'
require 'set'
require 'nokogiri'
CARDS_PATH = '~/Library/Application Support/Cockatrice/Cockatrice/cards.xml'
Database = Struct.new(:sets, :cards) do
def self.load
doc = open(File.expand_path(CARDS_PATH)) { |f| Nokogiri::XML(f) }
sets_doc = doc.at('/cockatrice_carddatabase/sets')
cards_doc = doc.at('/cockatrice_carddatabase/cards')
sets = sets_doc.element_children.map { |set_doc| MagicSet.from_doc(set_doc) }
cards = cards_doc.element_children.flat_map { |card_doc| MagicCard.all_from_doc(card_doc) }
new(sets, cards)
end
def set(id)
sets.find { |s| s.id == id }
end
def pool(set_ids, colors)
set_ids = set_ids.to_set
cards.select { |c| !c.basic_land? && set_ids.include?(c.set_id) && (c.colors - colors).empty? }
end
def basic_lands(set_ids)
names_to_colors = {
'Plains' => 'W',
'Island' => 'U',
'Forest' => 'G',
'Mountain' => 'R',
'Swamp' => 'B',
}
cards.select { |c| c.basic_land? && set_ids.include?(c.set_id) }.group_by { |c| names_to_colors[c.name] }
end
end
MagicSet = Struct.new(:id, :name, :type, :release_date) do
def self.from_doc(doc)
id = doc.at('./name').inner_text
name = doc.at('./longname').inner_text
type = doc.at('./settype').inner_text
release_date = doc.at('./releasedate').inner_text
new(id, name, type, release_date)
end
end
MagicCard = Struct.new(:name, :colors, :set_id, :rarity, :type) do
def self.all_from_doc(doc)
name = doc.at('./name').inner_text
colors = doc.search('./color').map(&:inner_text)
colors = ['X'] if colors.empty?
type = doc.at('./type').inner_text
doc.search('./set').map do |set_element|
set_id = set_element.inner_text
rarity = set_element['rarity']
new(name, colors, set_id, rarity, type)
end
end
def basic_land?
type =~ /\ABasic (?:Snow )?Land — /
end
end
class App
def initialize
@db = Database.load
end
attr_reader :db
def run(args)
parse_args(args)
case @command
when 'list sets'
list_sets
when 'debug'
require 'pry'
binding.pry
else
deck = draw_deck
deck.group_by(&:name).each do |name, cards|
puts "#{cards.count} #{name}"
end
end
end
attr_reader :list_sets, :set_ids, :colors, :num_cards, :land_percent
def parse_args(args)
@command = 'draw'
@set_ids = ['ORI']
@colors = %w[W G U R B]
@num_cards = 60
@land_percent = 40
OptionParser.new do |parser|
parser.on '--list-sets', '-L' do
@command = 'list sets'
end
parser.on '--debug', '-D' do
@command = 'debug'
end
parser.on '--sets SETS', '-s' do |value|
@set_ids = value.split(',')
end
parser.on '--colors COLORS', '-c' do |value|
normalized = value.upcase.scan(/[A-Z]/).join
bad = normalized.tr('WGURBX', '').empty? or
abort "invalid colors: #{bad}"
@colors = normalized.split('')
end
parser.on '--num-cards N', '-n' do |value|
@num_cards = value.to_i
end
parser.on '--land-percent PERCENT', '-l' do |value|
@land_percent = value.to_f
end
end.parse!(args)
end
def list_sets
type_symbols = {'Core' => '*', 'Expansion' => ' '}
db.sets.sort_by(&:release_date).each do |set|
next if set.id == 'TK'
next if !type_symbols.key?(set.type)
puts "#{set.release_date} #{set.id} #{type_symbols[set.type]} #{set.name}"
end
end
def draw_deck
cards_by_rarity = db.pool(set_ids, colors).group_by(&:rarity)
basic_lands = db.basic_lands(set_ids)
num_lands = (0.01 * land_percent * num_cards).round
num_nonlands = num_cards - num_lands
num_rares = (1 * num_nonlands / 12.0).round
num_uncommons = (3 * num_nonlands / 12.0).round
num_commons = (8 * num_nonlands / 12.0).round
num_mythic_rares = num_rares.times.count { cards_by_rarity['Mythic Rare'] && rand < 0.125 }
num_rares -= num_mythic_rares
unless @silent
STDERR.puts '-'*80
STDERR.puts "Drawing #{num_mythic_rares} mythic rares from pool of #{cards_by_rarity['Mythic Rare'].size}" if cards_by_rarity['Mythic Rare']
STDERR.puts "Drawing #{num_rares} rares from pool of #{cards_by_rarity['Rare'].size}"
STDERR.puts "Drawing #{num_uncommons} uncommons from pool of #{cards_by_rarity['Uncommon'].size}"
STDERR.puts "Drawing #{num_commons} commons from pool of #{cards_by_rarity['Common'].size}"
STDERR.puts "Drawing #{num_lands} basic lands"
STDERR.puts '-'*80
end
mythic_rares = cards_by_rarity['Mythic Rare'] ? sample(cards_by_rarity['Mythic Rare'], num_mythic_rares) : []
rares = sample(cards_by_rarity['Rare'], num_rares)
uncommons = sample(cards_by_rarity['Uncommon'], num_uncommons)
commons = sample(cards_by_rarity['Common'], num_commons)
basic_lands = basic_lands_for(mythic_rares + rares + uncommons + commons, num_lands, basic_lands)
# color_sampler = (mythic_rares + rares + uncommons + commons).reject { |c| c.colors == ['X'] }.map(&:colors)
# color_sampler = [%W[W G U R B]] if color_sampler.empty?
# basic_lands = num_lands.times.map { basic_lands[color_sampler.sample.sample].sample }
mythic_rares + rares + uncommons + commons + basic_lands
end
def sample(available, count)
(count / available.size).times.flat_map { available } +
available.sample(count)
end
def basic_lands_for(cards, num_lands, basic_lands)
color_weights = Hash.new { |h, k| h[k] = 0 }
cards.each { |card| card.colors.each { |color| color_weights[color] += 1 } }
weight_remaining = color_weights.values.inject(0, :+).to_f
color_weights.flat_map do |color, weight|
num_lands_for_color = (num_lands * weight / weight_remaining).round
weight_remaining -= weight
num_lands -= num_lands_for_color
num_lands_for_color.times.map { basic_lands[color].sample }
end
end
end
App.new.run(ARGV)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment