# frozen_string_literal: true module ClashDeckGenerator2 module Services class DeckGenerator DEFAULT_RULES = { deck_size: 8, min_troops: 1, max_buildings: 2, min_avg_elixir: 2.5, max_avg_elixir: 5.0, min_meta_cards: 4, max_meta_cards: 8, min_win_conditions: 1, min_anti_air: 1, min_spell_roles: 1 }.freeze SPECIAL_COMBINATIONS = [ { evolutions: 1, specials: 2 }, { evolutions: 2, specials: 1 } ].freeze def initialize( cards_repo: ClashDeckGenerator2::Repos::CardsRepo.new, roles_repo: ClashDeckGenerator2::Services::CardRolesRepository.new, rules: {} ) @cards_repo = cards_repo @roles_repo = roles_repo @rules = DEFAULT_RULES.merge(rules) end def call cards = unwrap_cards(@cards_repo.all) raise "No cards found in DB" if cards.empty? raise "Not enough cards to build a deck" if cards.size < @rules[:deck_size] evolution_cards = cards.select { |card| card.rarity == "evolution" } special_cards = cards.select { |card| %w[hero champion].include?(card.rarity) } regular_cards = cards.reject { |card| %w[evolution hero champion].include?(card.rarity) } attempts = 5000 attempts.times do combo = SPECIAL_COMBINATIONS.sample next if evolution_cards.size < combo[:evolutions] next if special_cards.size < combo[:specials] chosen_evolutions = evolution_cards.sample(combo[:evolutions]) chosen_specials = special_cards.sample(combo[:specials]) used_names = (chosen_evolutions + chosen_specials).map(&:name) remaining_pool = regular_cards.reject { |card| used_names.include?(card.name) } remaining_needed = @rules[:deck_size] - chosen_evolutions.size - chosen_specials.size next if remaining_pool.size < remaining_needed deck = build_deck( chosen_evolutions: chosen_evolutions, chosen_specials: chosen_specials, remaining_pool: remaining_pool ) next if deck.nil? next unless valid_deck?(deck) return deck.shuffle end raise "Failed to generate a valid deck after #{attempts} attempts" end private def unwrap_cards(result) if result.is_a?(Array) && result.first == :cards result.last else result end end def build_deck(chosen_evolutions:, chosen_specials:, remaining_pool:) deck = chosen_evolutions + chosen_specials used_names = deck.map(&:name) deck = add_required_role_card(deck, remaining_pool, used_names, "spell") return nil if deck.nil? used_names = deck.map(&:name) deck = add_required_type_card(deck, remaining_pool, used_names, "troop") return nil if deck.nil? used_names = deck.map(&:name) deck = add_required_role_card(deck, remaining_pool, used_names, "win_condition") return nil if deck.nil? used_names = deck.map(&:name) deck = add_required_role_card(deck, remaining_pool, used_names, "anti_air") return nil if deck.nil? used_names = deck.map(&:name) deck = add_required_meta_cards(deck, remaining_pool, used_names) return nil if deck.nil? used_names = deck.map(&:name) cards_left = @rules[:deck_size] - deck.size return nil if cards_left.negative? filler_pool = remaining_pool.reject { |card| used_names.include?(card.name) } return nil if filler_pool.size < cards_left deck + filler_pool.sample(cards_left) end def add_required_role_card(deck, pool, used_names, role) return deck if deck.any? { |card| has_role?(card, role) } candidates = pool.select do |card| !used_names.include?(card.name) && has_role?(card, role) end return nil if candidates.empty? deck + [candidates.sample] end def add_required_type_card(deck, pool, used_names, type) return deck if deck.any? { |card| card.type == type } candidates = pool.select do |card| !used_names.include?(card.name) && card.type == type end return nil if candidates.empty? deck + [candidates.sample] end def add_required_meta_cards(deck, pool, used_names) current_meta = meta_cards_count(deck) needed_meta = @rules[:min_meta_cards] - current_meta return deck if needed_meta <= 0 meta_candidates = pool.select do |card| !used_names.include?(card.name) && meta_card?(card) end return nil if meta_candidates.size < needed_meta deck + meta_candidates.sample(needed_meta) end def valid_deck?(deck) return false unless deck.size == @rules[:deck_size] return false unless unique_cards?(deck) return false unless enough_troops?(deck) return false unless buildings_limit_ok?(deck) return false unless avg_elixir_ok?(deck) return false unless special_slots_ok?(deck) return false unless enough_win_conditions?(deck) return false unless enough_anti_air?(deck) return false unless enough_spell_roles?(deck) return false unless meta_cards_ok?(deck) true end def unique_cards?(deck) deck.map(&:name).uniq.size == deck.size end def enough_troops?(deck) deck.count { |card| card.type == "troop" } >= @rules[:min_troops] end def buildings_limit_ok?(deck) deck.count { |card| card.type == "building" } <= @rules[:max_buildings] end def avg_elixir_ok?(deck) avg = deck.sum(&:elixir_cost).to_f / deck.size avg >= @rules[:min_avg_elixir] && avg <= @rules[:max_avg_elixir] end def special_slots_ok?(deck) evolutions_count = deck.count { |card| card.rarity == "evolution" } special_count = deck.count { |card| %w[hero champion].include?(card.rarity) } [[1, 2], [2, 1]].include?([evolutions_count, special_count]) end def enough_win_conditions?(deck) deck.count { |card| has_role?(card, "win_condition") } >= @rules[:min_win_conditions] end def enough_anti_air?(deck) deck.count { |card| has_role?(card, "anti_air") } >= @rules[:min_anti_air] end def enough_spell_roles?(deck) deck.count { |card| has_role?(card, "spell") } >= @rules[:min_spell_roles] end def meta_cards_ok?(deck) count = meta_cards_count(deck) count >= @rules[:min_meta_cards] && count <= @rules[:max_meta_cards] end def meta_cards_count(deck) deck.count { |card| meta_card?(card) } end def meta_card?(card) card.is_meta == 1 end def has_role?(card, role) @roles_repo.has_role?(card.name, role) end end end end