lib/cw_card_utils/decklist_parser/deck.rb
# frozen_string_literal: true module CwCardUtils module DecklistParser # A Deck is a collection of cards. class Deck IGNORED_TRIBAL_TYPES = %w[ human wizard soldier scout shaman warrior cleric rogue advisor knight druid ].freeze def initialize(cmc_data_source) @main = [] @sideboard = [] @lands = [] @x_to_cast = [] @cmc_data_source = cmc_data_source end def to_h { mainboard: main.map(&:to_h), sideboard: sideboard.map(&:to_h), tribe: tribe, } end def color_identity @color_identity ||= main.map(&:color_identity).flatten.uniq end def color_identity_string @color_identity_string ||= CwCardUtils::DecklistParser::ColorIdentityResolver.resolve(color_identity) end def to_json(*_args) to_h.to_json end def main tag_cards(@main) end def sideboard tag_cards(@sideboard) end def tag_cards(cards) cards.map do |c| c.tags = CwCardUtils::DecklistParser::CardTagger.new(c, self).tags c end end def archetype @archetype ||= CwCardUtils::DecklistParser::ArchetypeDetector.new(self).detect end def inspect "<Deck: main: #{mainboard_size} sideboard: #{sideboard_size} lands: #{lands_count} x_to_cast: #{x_to_cast_count} cards: #{cards_count}>" end def collapsed_curve @collapsed_curve ||= CurveCalculator.new(self).collapsed_curve end def curve @curve ||= CurveCalculator.new(self).curve end def normalized_curve @normalized_curve ||= CurveCalculator.new(self).normalized_curve end def collapsed_normalized_curve @collapsed_normalized_curve ||= CurveCalculator.new(self).collapsed_normalized_curve end def empty? @main.empty? && @sideboard.empty? end def any? !empty? end def each(&block) @main.each do |c| block.call(c) end end def add(c, target = :mainboard) reset_counters card = Card.new(c[:name], c[:count], @cmc_data_source) card.tags = CwCardUtils::DecklistParser::CardTagger.new(card, self).tags if target == :mainboard @main << card else @sideboard << card end if card.type.include?("Land") @lands << card elsif card.cmc.nil? @x_to_cast << card end end def reset_counters @mainboard_size = nil @sideboard_size = nil @lands_count = nil @x_to_cast_count = nil @cards_count = nil end def format @format ||= detect_format_for_deck end def detect_format_for_deck if mainboard_size >= 100 :commander elsif mainboard_size >= 60 # Check if this looks like Commander (60+ cards, singleton except for basic lands) if mainboard_size >= 60 && is_singleton_deck? :commander else :standard end else :modern end end def is_singleton_deck? # Count non-basic land cards and check for duplicates non_basic_lands = @main.reject { |card| is_basic_land?(card) } # Check if any non-basic land card appears more than once card_counts = Hash.new(0) non_basic_lands.each do |card| card_counts[card.name] += card.count end # All non-basic land cards should appear only once card_counts.values.all? { |count| count == 1 } end def is_basic_land?(card) basic_land_names = %w[Plains Island Swamp Mountain Forest Wastes] basic_land_names.include?(card.name) end def mainboard_size @mainboard_size ||= main.sum { |card| card.count } end def sideboard_size @sideboard_size ||= sideboard.sum { |card| card.count } end def lands_count @lands.sum { |card| card.count } end def x_to_cast_count @x_to_cast.sum { |card| card.count } end def cards_count mainboard_size + sideboard_size end def count_without_lands cards_count - lands_count end def size cards_count end def tribe return @tribe if @tribe @tribe = detect_tribe_for_deck end def detect_tribe_for_deck subtype_counts = Hash.new(0) total_creatures = 0 @main.each do |card| next unless card.type&.include?("Creature") total_creatures += 1 subtypes = card.type.split(/[—-]/).last.to_s.strip.split subtypes.each do |type| subtype_counts[type.downcase] += 1 end end return nil if total_creatures < 6 || subtype_counts.empty? most_common = subtype_counts.max_by { |_, count| count } return nil if most_common.nil? dominant_type = most_common[0] count = most_common[1] # Suppress only if it's a boring type *and* barely dominant if IGNORED_TRIBAL_TYPES.include?(dominant_type) && count.to_f / total_creatures < 0.7 return nil end return dominant_type.to_sym if count.to_f / total_creatures >= 0.4 nil end end end end